@nitra/cursor 12.3.1 → 12.3.3

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.
Files changed (41) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +36 -19
  2. package/.claude-template/hooks/lib/tooling-only.sh +16 -0
  3. package/CHANGELOG.md +12 -0
  4. package/package.json +1 -1
  5. package/rules/bun/bun.mdc +5 -5
  6. package/rules/bun/policy/package_json/package_json.rego +0 -31
  7. package/rules/ga/ga.mdc +2 -4
  8. package/rules/ga/policy/lint_ga/lint_ga.rego +2 -2
  9. package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +1 -1
  10. package/rules/js-lint/js-lint.mdc +2 -5
  11. package/rules/js-lint/policy/lint_js_yml/template/lint-js.yml.snippet.yml +1 -5
  12. package/rules/js-lint/policy/package_json/template/package.json.snippet.json +0 -3
  13. package/rules/lint/js/docs/orchestrate.md +11 -12
  14. package/rules/lint/js/orchestrate.mjs +62 -3
  15. package/rules/python/js/docs/index.md +1 -0
  16. package/rules/python/js/docs/lint.md +21 -0
  17. package/rules/python/js/lint.mjs +14 -0
  18. package/rules/python/lint/docs/lint.md +15 -312
  19. package/rules/python/lint/lint.mjs +11 -5
  20. package/rules/python/meta.json +1 -1
  21. package/rules/rego/rego.mdc +2 -6
  22. package/rules/security/policy/package_json/package_json.rego +0 -19
  23. package/rules/security/security.mdc +5 -6
  24. package/rules/style-lint/policy/lint_style_yml/template/lint-style.yml.snippet.yml +1 -1
  25. package/rules/style-lint/policy/package_json/package_json.rego +0 -10
  26. package/rules/style-lint/style-lint.mdc +4 -6
  27. package/rules/text/js/formatting.mjs +7 -31
  28. package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +1 -1
  29. package/scripts/lib/fix/docs/llm-worker.md +2 -2
  30. package/scripts/lib/fix/docs/orchestrator.md +3 -2
  31. package/scripts/lib/fix/llm-worker.mjs +8 -5
  32. package/scripts/lib/fix/orchestrator.mjs +26 -6
  33. package/rules/ga/policy/package_json/package_json.rego +0 -20
  34. package/rules/ga/policy/package_json/target.json +0 -8
  35. package/rules/ga/policy/package_json/template/package.json.contains.json +0 -1
  36. package/rules/rego/policy/package_json/package_json.rego +0 -16
  37. package/rules/rego/policy/package_json/target.json +0 -5
  38. package/rules/rego/policy/package_json/template/package.json.snippet.json +0 -1
  39. package/rules/security/policy/package_json/template/package.json.contains.json +0 -1
  40. package/rules/security/policy/package_json/template/package.json.snippet.json +0 -5
  41. package/rules/style-lint/policy/package_json/template/package.json.contains.json +0 -5
@@ -91,26 +91,38 @@ if [[ -z "$TRANSCRIPT" ]]; then
91
91
  exit 0
92
92
  fi
93
93
 
94
+ # Файли, змінені в сесії (file_path із tool_use Edit/Write/MultiEdit) — спільне
95
+ # джерело для structural-скіпів нижче.
96
+ CHANGED_FILES=$(jq -r '
97
+ select(.type == "assistant" or .role == "assistant")
98
+ | .message as $m
99
+ | ($m.content // [])
100
+ | if type == "array" then
101
+ map(select(.type == "tool_use" and (.name == "Edit" or .name == "Write" or .name == "MultiEdit"))
102
+ | .input.file_path // empty)
103
+ | .[]
104
+ else empty end
105
+ ' "$TRANSCRIPT_PATH" 2>/dev/null | sort -u || true)
106
+
107
+ # Cross-project skip: якщо в сесії редагувалися файли, але ЖОДЕН не під $PROJECT_ROOT —
108
+ # це паралельна робота в іншому проєкті; ADR сюди не пишемо (чужі рішення не змішуємо).
109
+ # Сесії без редагувань (чисте Q&A / дизайн-дискусія) не відкидаємо — це валідний ADR.
110
+ # ENV `ADR_CAPTURE_SKIP_CROSS_PROJECT=0` вимикає скіп.
111
+ if [[ "${ADR_CAPTURE_SKIP_CROSS_PROJECT:-1}" = "1" && -n "$CHANGED_FILES" ]]; then
112
+ if ! printf '%s\n' "$CHANGED_FILES" | has_in_project_change "$PROJECT_ROOT"; then
113
+ log " → skipping ADR capture: cross-project session (no in-project changes)"
114
+ log " files: $(printf '%s' "$CHANGED_FILES" | tr '\n' ' ')"
115
+ exit 0
116
+ fi
117
+ fi
118
+
94
119
  # Structural skip: якщо в сесії змінювалися лише tooling-файли — не викликаємо LLM.
95
120
  # ENV `ADR_NORMALIZE_SKIP_TOOLING_ONLY=0` вимикає скіп.
96
- if [[ "${ADR_NORMALIZE_SKIP_TOOLING_ONLY:-1}" = "1" ]]; then
97
- CHANGED_FILES=$(jq -r '
98
- select(.type == "assistant" or .role == "assistant")
99
- | .message as $m
100
- | ($m.content // [])
101
- | if type == "array" then
102
- map(select(.type == "tool_use" and (.name == "Edit" or .name == "Write" or .name == "MultiEdit"))
103
- | .input.file_path // empty)
104
- | .[]
105
- else empty end
106
- ' "$TRANSCRIPT_PATH" 2>/dev/null | sort -u || true)
107
-
108
- if [[ -n "$CHANGED_FILES" ]]; then
109
- if printf '%s\n' "$CHANGED_FILES" | is_tooling_only_change "$PROJECT_ROOT"; then
110
- log " → skipping ADR capture: tooling-only session"
111
- log " files: $(printf '%s' "$CHANGED_FILES" | tr '\n' ' ')"
112
- exit 0
113
- fi
121
+ if [[ "${ADR_NORMALIZE_SKIP_TOOLING_ONLY:-1}" = "1" && -n "$CHANGED_FILES" ]]; then
122
+ if printf '%s\n' "$CHANGED_FILES" | is_tooling_only_change "$PROJECT_ROOT"; then
123
+ log " skipping ADR capture: tooling-only session"
124
+ log " files: $(printf '%s' "$CHANGED_FILES" | tr '\n' ' ')"
125
+ exit 0
114
126
  fi
115
127
  fi
116
128
 
@@ -166,7 +178,12 @@ TRANSCRIPT FOLLOWS:
166
178
  EOF
167
179
  )
168
180
 
169
- PROMPT_FULL=$(printf '%s\n%s\n' "$PROMPT" "$TRANSCRIPT")
181
+ # Scope: обмежуємо рішення поточним проєктом. Для змішаних сесій (правки і тут, і в
182
+ # чужих репо) детермінований cross-project gate не спрацьовує, тож звужуємо обсяг у промпті.
183
+ # Йде ПЕРЕД інструкціями, щоб не сприйматись як перший рядок транскрипту.
184
+ SCOPE_LINE="CURRENT PROJECT ROOT: $PROJECT_ROOT
185
+ SCOPE: Document ONLY decisions evidenced by changes within this project root. Ignore edits and discussion about files outside it (parallel work in other repositories)."
186
+ PROMPT_FULL=$(printf '%s\n\n%s\n%s\n' "$SCOPE_LINE" "$PROMPT" "$TRANSCRIPT")
170
187
 
171
188
  CLAUDE_MODEL="${CAPTURE_DECISIONS_CLAUDE_MODEL:-sonnet}"
172
189
  CURSOR_MODEL="${CAPTURE_DECISIONS_CURSOR_MODEL:-claude-4.6-sonnet-medium}"
@@ -39,6 +39,22 @@ is_tooling_only_change() {
39
39
  return 1
40
40
  }
41
41
 
42
+ # Cross-project guard: чи серед змінених файлів є хоч один під $proj.
43
+ # Вхід: рядки-шляхи у stdin (абсолютні file_path із tool_use).
44
+ # Вихід: 0 — є хоч один файл під $proj; 1 — жодного (сесія цілком в інших проєктах).
45
+ # Призначення: відсікти ADR-чернетки від паралельної роботи в чужих репозиторіях.
46
+ has_in_project_change() {
47
+ local proj="$1"
48
+ local f
49
+ while IFS= read -r f; do
50
+ [ -z "$f" ] && continue
51
+ case "$f" in
52
+ "$proj"/*) return 0 ;;
53
+ esac
54
+ done
55
+ return 1
56
+ }
57
+
42
58
  # Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
43
59
  # Поза git-репо або при помилці — вертаємо 1 (не tooling).
44
60
  git_diff_only_version_field() {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.3.3] - 2026-06-20
4
+
5
+ ### Changed
6
+
7
+ - ♻️ refactor(pipeline): Оновлено логіку ADR-захоплення та цілісність збірки
8
+
9
+ ## [12.3.2] - 2026-06-20
10
+
11
+ ### Fixed
12
+
13
+ - fix-каскад: per-tier timeout (локалі fail-fast ~45s замість стіни 120s, env N_LOCAL_FIX_TIMEOUT_MS/N_CLOUD_FIX_TIMEOUT_MS) + хмарний транспортний збій (pi ETIMEDOUT/spawn) обриває драбину замість ескалації на cloud-avg — не палиться avg-бюджет
14
+
3
15
  ## [12.3.1] - 2026-06-20
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.3.1",
3
+ "version": "12.3.3",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
package/rules/bun/bun.mdc CHANGED
@@ -66,10 +66,10 @@ FROM oven/bun:alpine AS build-env
66
66
 
67
67
  ## lint
68
68
 
69
- Якщо в кореневому @package.json існують скрипти з префіксом `lint-`, обов'язково створюй `lint` скрипт, який буде запускати всі ці скрипти з лінт-префіксом.
69
+ Лінт запускається через CLI **`n-cursor`**, **не** через `package.json`-скрипти:
70
70
 
71
- У кінці скрипта `lint` додай `&& oxfmt .`.
71
+ - **`n-cursor lint --full`** — весь репо: усі правила (per-file лінтери + конформність) + `oxfmt` у кінці (fix-режим);
72
+ - **`n-cursor lint`** — дельта vs origin (per-file лінтери лише змінених файлів);
73
+ - **`n-cursor lint <rule…>`** — конкретні правила (лінтер + конформність), напр. **`n-cursor lint ga`**.
72
74
 
73
- Якщо в **`.n-cursor.json`** у масиві **`rules`** є **`docker`**, у кореневому `package.json` **обов'язково** скрипт **`lint-docker`** (див. **`docker.mdc`**) і рядок **`bun run lint-docker`** у **`lint`**. Якщо є **`k8s`** — **обов'язково** **`lint-k8s`** і **`bun run lint-k8s`** у **`lint`** (див. **`k8s.mdc`**).
74
-
75
- **Зворотній інваріант:** якщо правила **немає** в `rules` (або воно явно перенесене в **`disable-rules`**), скрипту **`lint-<id>`** у кореневому `package.json` бути **не може**, і ланцюжок агрегованого **`scripts.lint`** не має містити **`bun run lint-<id>`**. Інакше `bun run lint` падатиме на вимкненому правилі — `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево незалежно від `rules`/`disable-rules`. Для скриптів із кількома власниками (як **`lint-image`** — обслуговує і **`image-avif`**, і **`image-compress`**) скрипт лишається дозволеним, поки активний **хоч один** власник; зворотній інваріант тригериться лише коли в `rules` немає **жодного** з них. Перевірка — **`npx @nitra/cursor fix bun`**.
75
+ У кореневому `package.json` **не повинно бути** `lint`/`lint-*` скриптів єдина точка лінту CLI `n-cursor`. У CI кожен workflow викликає **`n-cursor lint <rule> --read-only`** напряму (без обгорток).
@@ -8,7 +8,6 @@
8
8
  # - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для `n-cursor coverage`
9
9
  # (правило `test` enabled завжди — див. `test/auto.md`; published workspace-и не мають
10
10
  # `devDependencies` за `npm-module.mdc`)
11
- # - Агрегований `lint` скрипт (cross-script aggregation logic)
12
11
  #
13
12
  # Перевірки, які ЗАЛИШИЛИСЬ у JS (потребують FS / cross-file):
14
13
  # - `lint-docker` / `lint-k8s` коли `.n-cursor.json:rules` містить відповідне
@@ -17,13 +16,6 @@ package bun.package_json
17
16
 
18
17
  import rego.v1
19
18
 
20
- # ── Шаблони повідомлень ────────────────────────────────────────────────────
21
-
22
- lint_aggregate_missing_template := concat(" ", [
23
- "У package.json є скрипти %v, але немає агрегованого `lint`.",
24
- "Додай скрипт, який запускає їх через `bun run` (bun.mdc)",
25
- ])
26
-
27
19
  # ── deny: заборонені top-level поля (template-driven) ─────────────────────
28
20
 
29
21
  # Сентинельний value відрізняє «поле відсутнє» від «поле є з будь-яким значенням»
@@ -43,29 +35,6 @@ deny contains msg if {
43
35
  msg := sprintf("Кореневі devDependencies: дозволені лише @nitra/* або root-only test peers — прибери або перенеси: %s (bun.mdc)", [name])
44
36
  }
45
37
 
46
- # ── deny: агрегований lint-скрипт (cross-script aggregation logic) ───────
47
-
48
- deny contains msg if {
49
- count(lint_prefixed_scripts) > 0
50
- lint_script == ""
51
- msg := sprintf(lint_aggregate_missing_template, [lint_prefixed_scripts])
52
- }
53
-
54
- deny contains msg if {
55
- count(lint_prefixed_scripts) > 0
56
- lint_script != ""
57
- some script in lint_prefixed_scripts
58
- not contains(lint_script, sprintf("bun run %s", [script]))
59
- msg := sprintf("Скрипт `lint` має викликати `%s` через `bun run` (bun.mdc)", [script])
60
- }
61
-
62
- deny contains msg if {
63
- count(lint_prefixed_scripts) > 0
64
- lint_script != ""
65
- not regex.match(`&&[ \t]+oxfmt[ \t]+\.[ \t]*$`, lint_script)
66
- msg := "Скрипт `lint` має закінчуватися на `&& oxfmt .` (bun.mdc)"
67
- }
68
-
69
38
  # ── helpers ────────────────────────────────────────────────────────────────
70
39
 
71
40
  allowed_root_test_deps := {"vitest", "@vitest/coverage-v8", "@stryker-mutator/vitest-runner", "@playwright/test"}
package/rules/ga/ga.mdc CHANGED
@@ -118,11 +118,9 @@ concurrency:
118
118
  - uses: ./.github/actions/setup-bun-deps
119
119
  ```
120
120
 
121
- **Лінт:** [actionlint](https://github.com/rhysd/actionlint) через [github-actionlint](https://www.npmjs.com/package/github-actionlint); [zizmor](https://docs.zizmor.sh) — `uvx`, офлайн. Канонічний скрипт у корені делегує виконання CLI `n-cursor lint-ga` (бінарка з `node_modules/.bin/` пакету `@nitra/cursor`), який робить preflight на `shellcheck` і послідовно запускає `actionlint` та `zizmor`:
121
+ **Лінт:** [actionlint](https://github.com/rhysd/actionlint) через [github-actionlint](https://www.npmjs.com/package/github-actionlint); [zizmor](https://docs.zizmor.sh) — `uvx`, офлайн. Запуск через **`n-cursor lint ga`** (CI — `--read-only`; бінарка з `node_modules/.bin/` пакету `@nitra/cursor`), який робить preflight на `shellcheck` і послідовно запускає `actionlint` та `zizmor`. Окремого `package.json`-скрипта немає.
122
122
 
123
- - `package.json` `scripts.lint-ga` має містити `n-cursor lint-ga`: [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
124
-
125
- > Не використовуй `npx --no @nitra/cursor lint-ga` — `bun run` автоматично транслює `npx` у `bun x`, а `bun x` для скоупованого пакету з одним bin-ім’ям повертає 0 без виконання. Виклик через bin-ім’я `n-cursor` працює і у `bun run`, і у `npm run`.
123
+ > Виклик через bin-ім’я `n-cursor` (а **не** `npx @nitra/cursor`): `bun x`/`npx` для скоупованого пакету з одним bin-ім’ям повертає 0 без виконання, тому в CI-кроці `run:` використовуй саме `n-cursor lint <rule>`.
126
124
 
127
125
  CLI робить preflight на `shellcheck` і `uv` (`uvx`) у `PATH`, потім запускає `bunx github-actionlint` і `uvx zizmor --offline --collect=workflows .`.
128
126
 
@@ -100,8 +100,8 @@ deny contains msg if {
100
100
 
101
101
  deny contains msg if {
102
102
  expected_run_blob != ""
103
- not contains(job_run_blob, "bun run lint-ga")
104
- msg := "lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)"
103
+ not contains(job_run_blob, "n-cursor lint ga --read-only")
104
+ msg := "lint-ga.yml: має бути крок run: n-cursor lint ga --read-only (ga.mdc)"
105
105
  }
106
106
 
107
107
  # ── helpers ────────────────────────────────────────────────────────────────
@@ -38,4 +38,4 @@ jobs:
38
38
  | sudo tar -xz -C /usr/local/bin conftest
39
39
 
40
40
  - name: Lint GA
41
- run: bun run lint-ga
41
+ run: n-cursor lint ga --read-only
@@ -5,23 +5,20 @@ alwaysApply: false
5
5
  version: '1.30'
6
6
  ---
7
7
 
8
- **oxlint**, **ESLint**, **jscpd**, **knip**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`**, **`bunx knip`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config`** — версія не нижче канонічного мінімуму зі snippet нижче (semver-поріг, єдине джерело істини) (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint). Dependency-політику CI-етапу (`@e18e/eslint-plugin` і oxlint/eslint/jscpd/knip окремо не додавати) винесено в `js-lint-ci`.
8
+ **oxlint**, **ESLint**, **jscpd**, **knip**. Запуск **`n-cursor lint js-lint js-lint-ci`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config`** — версія не нижче канонічного мінімуму зі snippet нижче (semver-поріг, єдине джерело істини) (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint). Dependency-політику CI-етапу (`@e18e/eslint-plugin` і oxlint/eslint/jscpd/knip окремо не додавати) винесено в `js-lint-ci`.
9
9
 
10
10
  У кожному **`package.json`** проєкту (корінь і всі workspace-пакети) має бути **`"type": "module"`** — весь код у ESM.
11
11
 
12
12
  ```json title="package.json"
13
13
  {
14
14
  "type": "module",
15
- "scripts": {
16
- "lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip --no-config-hints"
17
- },
18
15
  "devDependencies": {
19
16
  "@nitra/eslint-config": "^3.10.0"
20
17
  }
21
18
  }
22
19
  ```
23
20
 
24
- Канон `type` + `scripts.lint-js` (substring requirement) і мінімальна `@nitra/eslint-config` (semver-поріг `devDependencies`): [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
21
+ Канон `type` і мінімальна `@nitra/eslint-config` (semver-поріг `devDependencies`): [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json). Окремого `lint-js` скрипта немає — лінт через **`n-cursor lint js-lint js-lint-ci`** (CI — `--read-only`).
25
22
 
26
23
  ## Розширення нових файлів — `.mjs` / `.cjs`, не `.js`
27
24
 
@@ -37,8 +37,4 @@ jobs:
37
37
  - uses: ./.github/actions/setup-bun-deps
38
38
 
39
39
  - name: Eslint
40
- run: |
41
- bunx oxlint
42
- bunx eslint .
43
- bunx jscpd .
44
- bunx knip --no-config-hints
40
+ run: n-cursor lint js-lint js-lint-ci --read-only
@@ -1,8 +1,5 @@
1
1
  {
2
2
  "type": "module",
3
- "scripts": {
4
- "lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip --no-config-hints"
5
- },
6
3
  "devDependencies": {
7
4
  "@nitra/eslint-config": "^3.10.0"
8
5
  }
@@ -3,30 +3,29 @@ type: JS Module
3
3
  title: orchestrate.mjs
4
4
  resource: npm/rules/lint/js/orchestrate.mjs
5
5
  docgen:
6
- crc: 0ab5b22c
6
+ crc: b0c7a4c2
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
+ judgeModel: openai-codex/gpt-5.4-mini
9
10
  ---
10
11
 
11
- Оркестратор `n-cursor lint` визначає, які правила лінтування застосовувати, керуючись двома ортогональними осями: консолідацією правил та уніфікацією режиму `readonly`. Вибір правил відбувається на основі конфігурацій, зокрема файлів `meta.json`, які визначають обсяг дії (`per-file` чи `full`) для кожного правила. Запуск лінтування може сканувати лише змінені файли відносно origin (за замовчуванням) або весь репозиторій при використанні прапорця `--full`.
12
+ ## Огляд
13
+
14
+ Модуль відповідає за визначення та виконання процесу лінтування коду. Функція `selectLintRules` вибирає та сортує ідентифікатори правил лінтування на основі конфігурацій, визначених у meta.json. Функція `runLint` запускає перевірку обраних правил для змінених або всіх файлів репозиторію.
12
15
 
13
16
  ## Поведінка
14
17
 
15
- Поведінка
16
- selectLintRules вибирає і сортує ідентифікатори правил на основі їхнього обсягу дії (`per-file` або `full`) та прапорця `--full`.
17
- runLint запускає оркестрацію лінтування: або виконує перевірку конформності для заданих правил, або ітерує по алфавітно відсортованих правилах (`runPerFileRules`), запускаючи лінтер для змінених файлів (за замовчуванням), або виконує перевірку конформності всього репозиторію при використанні прапорця `--full`.
18
- **Fail-fast — лише в `--read-only`** (CI/детект): перший ненульовий код спиняє. У fix-режимі (default) ненульовий код per-file правила НЕ спиняє — проганяються всі правила й виконується крок виправлення (конформність-драбина), а повертається найгірший код.
19
- У режимі `--full` без `--read-only` після конформність-фази (`runFullConformancePhase`) друкується резюме викликів моделей за прогін (`reportRunStats`: локальна / cloud-min / cloud-avg) і викликається escalation-аналітика (`analyze-escalation.mjs`): фіксує зсув escalation-логу до фази, після — аналізує записи саме цього прогону. Жодне з цього не впливає на exit-код lint.
18
+ selectLintRules вибирає і сортує ідентифікатори правил для лінтування на основі їхніх конфігурацій, включаючи можливість включення правил, що застосовуються до всього репозиторію.
19
+ runLint запускає процес лінтування, виконуючи перевірку правил для змінених файлів або для всього репозиторію, залежно від наданих опцій, і може виконувати форматування.
20
20
 
21
21
  ## Публічний API
22
22
 
23
- selectLintRules — Вибирає ідентифікатори правил для контексту в алфавітному порядку.
23
+ selectLintRules — Вибирає ідентифікатори правил для контексту, упорядковані за алфавітом.
24
24
  runLint — Запускає процес лінтування.
25
- full — Сканує весь репозиторій порівняно з початковим станом.
25
+ full — Сканує весь репозиторій, порівнюючи його з початковим станом.
26
26
  readOnly — Виявляє проблеми без внесення змін.
27
- rules — Перевіряє лише відповідність заданого набору правил, ігноруючи повне сканування.
27
+ rules — Виконує повне сканування лише для вказаних правил у заданому обсязі.
28
28
 
29
29
  ## Гарантії поведінки
30
30
 
31
- - Read-only: файл не виконує операцій запису у файлову систему.
32
- - Не звертається до мережі.
31
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -3,9 +3,11 @@ import { existsSync, readdirSync } from 'node:fs'
3
3
  import { dirname, join } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { cwd as processCwd } from 'node:process'
6
+ import { spawnSync } from 'node:child_process'
6
7
 
7
8
  import { parseRuleLintSpec, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
8
9
  import { collectChangedFilesSince, resolveChangedBase } from '../../../scripts/lib/changed-files.mjs'
10
+ import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
9
11
 
10
12
  // Цей файл: npm/rules/lint/js/orchestrate.mjs → PACKAGE_ROOT = npm (чотири dirname угору).
11
13
  const PACKAGE_ROOT = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
@@ -120,12 +122,62 @@ async function runFullConformancePhase(cwd, readOnly, log) {
120
122
  return conformanceCode
121
123
  }
122
124
 
125
+ /**
126
+ * Формат-крок (`oxfmt .`): whole-tree форматування у fix-режимі. У read-only НЕ викликається
127
+ * (CI/детект — нуль мутацій). `oxfmt` форматує не лише JS, а й root-конфіги (toml тощо), тож
128
+ * крок незалежний від набору правил і scope. Якщо `oxfmt` відсутній у PATH — пропуск (не fail).
129
+ * @param {string} cwd корінь
130
+ * @param {(s: string) => void} log логер
131
+ * @returns {Promise<number>} код виходу oxfmt (0 — OK або пропущено)
132
+ */
133
+ async function runFormat(cwd, log) {
134
+ const oxfmt = resolveCmd('oxfmt')
135
+ if (!oxfmt) {
136
+ log('ℹ️ lint: oxfmt недоступний у PATH — формат-крок пропущено.\n')
137
+ return 0
138
+ }
139
+ const r = spawnSync(oxfmt, ['.'], { cwd, stdio: 'inherit', shell: false })
140
+ const code = typeof r.status === 'number' ? r.status : 1
141
+ if (code !== 0) log(`❌ lint: oxfmt — помилка (код ${code})\n`)
142
+ return code
143
+ }
144
+
145
+ /**
146
+ * Scoped-режим (`lint <rule…>`): повний прогін НАЗВАНИХ правил — їх лінтер (`js/lint.mjs`,
147
+ * whole-repo) для тих, що його мають, + конформність для всіх названих. Дзеркалить `--full`,
148
+ * але звужено до правил, тож `lint ga` ≡ standalone `lint-ga`. Конформність-only правила
149
+ * (напр. `changelog` із hk) не мають `js/lint.mjs` → проганяється лише їх конформність
150
+ * (зворотна сумісність із колишнім `fix <rule>`). oxfmt у scoped НЕ запускається — це
151
+ * таргетований прогін правил, а не глобальне форматування.
152
+ * @param {string[]} rules id названих правил
153
+ * @param {{ cwd: string, readOnly: boolean, rulesDir: string, conformance: boolean, log: (s: string) => void }} ctx контекст (`conformance` — чи запускати конформність; false для юніт-тестів із кастомним rulesDir, де реальний пакет недоступний)
154
+ * @returns {Promise<number>} найгірший код (read-only — fail-fast на першому ненульовому)
155
+ */
156
+ async function runScopedRules(rules, ctx) {
157
+ const { cwd, readOnly, rulesDir, conformance, log } = ctx
158
+ const metaById = readAllMeta(rulesDir)
159
+ const linterIds = rules.filter(id => existsSync(join(rulesDir, id, 'js', 'lint.mjs')))
160
+ let worst = 0
161
+ if (linterIds.length > 0) {
162
+ const perFile = await runPerFileRules(linterIds, { rulesDir, changed: undefined, cwd, readOnly, metaById, log })
163
+ if (perFile.stop) return perFile.code
164
+ worst = perFile.code
165
+ }
166
+ if (!conformance) return worst
167
+ const conformanceCode = await runConformance(cwd, readOnly, log, rules)
168
+ if (conformanceCode !== 0) {
169
+ if (readOnly) return conformanceCode
170
+ worst = conformanceCode
171
+ }
172
+ return worst
173
+ }
174
+
123
175
  /**
124
176
  * Запускає lint-оркестрацію.
125
177
  * @param {{ full?: boolean, readOnly?: boolean, rules?: string[], cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
126
178
  * - `full` — весь репо (`true`) проти дельти vs origin (`false`, default);
127
179
  * - `readOnly` — лише детект без мутацій (`true`) проти fix (`false`, default);
128
- * - `rules` — непорожній фільтр → лише конформність цих правил (без лінтер-скану; мапить `fix <rule>`).
180
+ * - `rules` — непорожній scopeповний прогін лише цих правил (лінтер + конформність, whole-repo).
129
181
  * @returns {Promise<number>} exit code
130
182
  */
131
183
  export async function runLint(opts = {}) {
@@ -136,9 +188,9 @@ export async function runLint(opts = {}) {
136
188
  const rulesDir = opts.rulesDir ?? RULES_DIR
137
189
  const log = opts.log ?? (s => process.stdout.write(s))
138
190
 
139
- // Rule-filter режим (напр. `lint changelog` із hk): лише конформність указаних правил, без лінтерів.
191
+ // Scoped режим (`lint <rule…>`): повний прогін названих правил лінтер + конформність.
140
192
  if (rules.length > 0) {
141
- return runConformance(cwd, readOnly, log, rules)
193
+ return runScopedRules(rules, { cwd, readOnly, rulesDir, conformance: opts.rulesDir === undefined, log })
142
194
  }
143
195
 
144
196
  // Default scope — дельта vs origin (merge-base main/origin/main); `--full` — весь репо.
@@ -163,5 +215,12 @@ export async function runLint(opts = {}) {
163
215
  worst = conformanceCode
164
216
  }
165
217
  }
218
+
219
+ // Формат-крок (oxfmt): fix-режим — завжди (будь-який scope); read-only пропускаємо (нуль
220
+ // мутацій). Кастомний rulesDir (юніт-тести) — реальний пакет недоступний, тож пропускаємо.
221
+ if (!readOnly && opts.rulesDir === undefined) {
222
+ const fmtCode = await runFormat(cwd, log)
223
+ if (fmtCode !== 0) worst = fmtCode
224
+ }
166
225
  return worst
167
226
  }
@@ -9,4 +9,5 @@ resource: npm/rules/python/js/
9
9
  | Файл | Тип |
10
10
  |---|---|
11
11
  | [applies.mjs](applies.md) | JS Module |
12
+ | [lint.mjs](lint.md) | JS Module |
12
13
  | [tooling.mjs](tooling.md) | JS Module |
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: JS Module
3
+ title: lint.mjs
4
+ resource: npm/rules/python/js/lint.mjs
5
+ docgen:
6
+ crc: a0d17a44
7
+ score: 100
8
+ ---
9
+
10
+ Оркестраторний адаптер правила `python` для `n-cursor lint`. Делегує перевірку наявній CLI-формі (`runLintPython` із `../lint/lint.mjs`). Режиму на рівні окремих файлів немає — `uv`/`ruff`/`mypy` працюють по всьому проєкту, тож параметр `files` ігнорується. За відсутності `pyproject.toml` у корені крок завершується успіхом без запуску інструментів.
11
+
12
+ ## Поведінка
13
+
14
+ 1. Викликає CLI-форму python-лінтера для всього проєкту.
15
+ 2. У `readOnly` пробрасує прапорець далі: `ruff` без `--fix`, `ruff format --check` (нуль мутацій для CI).
16
+ 3. Повертає код виходу інструменту.
17
+
18
+ ## Гарантії поведінки
19
+
20
+ - Read-only за наявності `readOnly`: інструменти не мутують робоче дерево.
21
+ - Не звертається до мережі (uv-кроки можуть, але це поведінка делегата, не цього модуля).
@@ -0,0 +1,14 @@
1
+ /** @see ./docs/lint.md */
2
+ import { runLintPython } from '../lint/lint.mjs'
3
+
4
+ /**
5
+ * Ci-крок python: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується;
6
+ * uv/ruff/mypy працюють по всьому проєкту). Без `pyproject.toml` крок — no-op (exit 0).
7
+ * @param {string[] | undefined} _files ігнорується (whole-project аналіз)
8
+ * @param {string} [_cwd] корінь (CLI бере process.cwd())
9
+ * @param {{ readOnly?: boolean }} [opts] readOnly → ruff без `--fix`, format `--check` (нуль мутацій)
10
+ * @returns {Promise<number>} exit code
11
+ */
12
+ export function lint(_files, _cwd, opts = {}) {
13
+ return runLintPython({ readOnly: opts.readOnly === true })
14
+ }