@nitra/cursor 12.3.2 → 12.4.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 +36 -19
- package/.claude-template/hooks/lib/tooling-only.sh +16 -0
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/rules/bun/bun.mdc +5 -5
- package/rules/bun/policy/package_json/package_json.rego +0 -31
- package/rules/ga/ga.mdc +2 -4
- package/rules/ga/policy/lint_ga/lint_ga.rego +2 -2
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +1 -1
- package/rules/js-lint/js-lint.mdc +2 -5
- package/rules/js-lint/policy/lint_js_yml/template/lint-js.yml.snippet.yml +1 -5
- package/rules/js-lint/policy/package_json/template/package.json.snippet.json +0 -3
- package/rules/lint/js/docs/orchestrate.md +11 -12
- package/rules/lint/js/orchestrate.mjs +82 -5
- package/rules/php/js/docs/index.md +1 -0
- package/rules/php/js/docs/lint.md +20 -0
- package/rules/php/js/lint.mjs +13 -0
- package/rules/php/php.mdc +1 -3
- package/rules/php/policy/lint_php_yml/template/lint-php.yml.snippet.yml +1 -1
- package/rules/python/js/docs/index.md +1 -0
- package/rules/python/js/docs/lint.md +21 -0
- package/rules/python/js/lint.mjs +14 -0
- package/rules/python/lint/docs/lint.md +15 -312
- package/rules/python/lint/lint.mjs +11 -5
- package/rules/python/meta.json +1 -1
- package/rules/python/policy/lint_python_yml/template/lint-python.yml.snippet.yml +1 -1
- package/rules/python/python.mdc +1 -3
- package/rules/rego/rego.mdc +2 -6
- package/rules/rust/js/docs/index.md +1 -0
- package/rules/rust/js/docs/lint.md +21 -0
- package/rules/rust/js/lint.mjs +67 -0
- package/rules/rust/rust.mdc +2 -4
- package/rules/security/policy/package_json/package_json.rego +0 -19
- package/rules/security/security.mdc +5 -6
- package/rules/style-lint/policy/lint_style_yml/template/lint-style.yml.snippet.yml +1 -1
- package/rules/style-lint/policy/package_json/package_json.rego +0 -10
- package/rules/style-lint/style-lint.mdc +4 -6
- package/rules/text/js/formatting.mjs +7 -31
- package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +1 -1
- package/rules/ga/policy/package_json/package_json.rego +0 -20
- package/rules/ga/policy/package_json/target.json +0 -8
- package/rules/ga/policy/package_json/template/package.json.contains.json +0 -1
- package/rules/php/policy/package_json/package_json.rego +0 -16
- package/rules/php/policy/package_json/target.json +0 -4
- package/rules/php/policy/package_json/template/package.json.contains.json +0 -5
- package/rules/python/policy/package_json/package_json.rego +0 -16
- package/rules/python/policy/package_json/target.json +0 -4
- package/rules/python/policy/package_json/template/package.json.contains.json +0 -5
- package/rules/rego/policy/package_json/package_json.rego +0 -16
- package/rules/rego/policy/package_json/target.json +0 -5
- package/rules/rego/policy/package_json/template/package.json.snippet.json +0 -1
- package/rules/rust/policy/package_json/package_json.rego +0 -18
- package/rules/rust/policy/package_json/target.json +0 -5
- package/rules/rust/policy/package_json/template/package.json.contains.json +0 -9
- package/rules/security/policy/package_json/template/package.json.contains.json +0 -1
- package/rules/security/policy/package_json/template/package.json.snippet.json +0 -5
- 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
|
|
98
|
-
|
|
99
|
-
|
|
|
100
|
-
|
|
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
|
-
|
|
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.4.0] - 2026-06-21
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Уніфіковано selection linter-фази: unscoped n-cursor lint бере правила з .n-cursor.json, а meta.json#lint лишається тільки scope-класифікацією.
|
|
8
|
+
|
|
9
|
+
## [12.3.3] - 2026-06-20
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- ♻️ refactor(pipeline): Оновлено логіку ADR-захоплення та цілісність збірки
|
|
14
|
+
|
|
3
15
|
## [12.3.2] - 2026-06-20
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
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
|
-
|
|
69
|
+
Лінт запускається через CLI **`n-cursor`**, **не** через `package.json`-скрипти:
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
- **`n-cursor lint --full`** — весь репо: активні у `.n-cursor.json` правила (per-file + full лінтери за `meta.json#lint` scope, конформність) + `oxfmt` у кінці (fix-режим);
|
|
72
|
+
- **`n-cursor lint`** — дельта vs origin (активні у `.n-cursor.json` per-file лінтери лише змінених файлів);
|
|
73
|
+
- **`n-cursor lint <rule…>`** — конкретні правила (лінтер + конформність), напр. **`n-cursor lint ga`**.
|
|
72
74
|
|
|
73
|
-
|
|
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`, офлайн.
|
|
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
|
-
- `
|
|
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, "
|
|
104
|
-
msg := "lint-ga.yml: має бути крок run:
|
|
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 ────────────────────────────────────────────────────────────────
|
|
@@ -5,23 +5,20 @@ alwaysApply: false
|
|
|
5
5
|
version: '1.30'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
**oxlint**, **ESLint**, **jscpd**, **knip**.
|
|
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`
|
|
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
|
|
|
@@ -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:
|
|
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
|
-
|
|
12
|
+
## Огляд
|
|
13
|
+
|
|
14
|
+
Модуль відповідає за визначення та виконання процесу лінтування коду. Unscoped linter-фаза бере активні правила з `.n-cursor.json`, а `meta.json#lint` використовує лише як класифікацію scope (`per-file` або `full`). Функція `runLint` запускає перевірку обраних правил для змінених або всіх файлів репозиторію.
|
|
12
15
|
|
|
13
16
|
## Поведінка
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
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 вибирає і сортує ідентифікатори правил для лінтування з уже активованого списку `.n-cursor.json`, включаючи можливість включення правил, що застосовуються до всього репозиторію.
|
|
19
|
+
runLint запускає процес лінтування, виконуючи перевірку активних правил для змінених файлів або для всього репозиторію, залежно від наданих опцій, і може виконувати форматування. Scoped режим `lint <rule…>` запускає названі правила напряму та не потребує `.n-cursor.json` для linter-фази.
|
|
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,12 @@ 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'
|
|
11
|
+
import { isRuleEnabled, readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
|
|
9
12
|
|
|
10
13
|
// Цей файл: npm/rules/lint/js/orchestrate.mjs → PACKAGE_ROOT = npm (чотири dirname угору).
|
|
11
14
|
const PACKAGE_ROOT = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
|
|
@@ -40,17 +43,33 @@ async function runConformance(cwd, readOnly, log, filter = []) {
|
|
|
40
43
|
* Вибирає id правил для контексту, алфавітно.
|
|
41
44
|
* @param {Record<string, {lint?: unknown}>} metaById мапа id → meta-обʼєкт
|
|
42
45
|
* @param {boolean} full `false` → лише `per-file` правила; `true` → усі (`per-file` ∪ `full`)
|
|
46
|
+
* @param {string[]} enabledRuleIds активні rule-id з `.n-cursor.json`
|
|
43
47
|
* @returns {string[]} відсортовані id
|
|
44
48
|
*/
|
|
45
|
-
export function selectLintRules(metaById, full) {
|
|
49
|
+
export function selectLintRules(metaById, full, enabledRuleIds) {
|
|
50
|
+
const enabled = new Set(enabledRuleIds)
|
|
46
51
|
const out = []
|
|
47
52
|
for (const [id, raw] of Object.entries(metaById)) {
|
|
53
|
+
if (!enabled.has(id)) continue
|
|
48
54
|
const scope = parseRuleLintSpec(raw?.lint)
|
|
49
55
|
if (scope === 'per-file' || (full && scope === 'full')) out.push(id)
|
|
50
56
|
}
|
|
51
57
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Активні правила для unscoped linter-фази. `.n-cursor.json` — єдине джерело
|
|
62
|
+
* whitelist/disable, `meta.json#lint` нижче використовується лише як scope (`per-file`/`full`).
|
|
63
|
+
* @param {Record<string, unknown>} metaById доступні bundled правила
|
|
64
|
+
* @param {string} cwd корінь
|
|
65
|
+
* @returns {Promise<string[]>} активні rule-id з конфіга, що існують у пакеті
|
|
66
|
+
*/
|
|
67
|
+
async function readEnabledLintRuleIds(metaById, cwd) {
|
|
68
|
+
const config = await readNCursorConfigLite(cwd)
|
|
69
|
+
if (!config.exists) return []
|
|
70
|
+
return Object.keys(metaById).filter(id => isRuleEnabled(config, id))
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
/**
|
|
55
74
|
* Зчитує meta всіх правил пакета.
|
|
56
75
|
* @param {string} rulesDir каталог rules
|
|
@@ -120,12 +139,62 @@ async function runFullConformancePhase(cwd, readOnly, log) {
|
|
|
120
139
|
return conformanceCode
|
|
121
140
|
}
|
|
122
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Формат-крок (`oxfmt .`): whole-tree форматування у fix-режимі. У read-only НЕ викликається
|
|
144
|
+
* (CI/детект — нуль мутацій). `oxfmt` форматує не лише JS, а й root-конфіги (toml тощо), тож
|
|
145
|
+
* крок незалежний від набору правил і scope. Якщо `oxfmt` відсутній у PATH — пропуск (не fail).
|
|
146
|
+
* @param {string} cwd корінь
|
|
147
|
+
* @param {(s: string) => void} log логер
|
|
148
|
+
* @returns {Promise<number>} код виходу oxfmt (0 — OK або пропущено)
|
|
149
|
+
*/
|
|
150
|
+
function runFormat(cwd, log) {
|
|
151
|
+
const oxfmt = resolveCmd('oxfmt')
|
|
152
|
+
if (!oxfmt) {
|
|
153
|
+
log('ℹ️ lint: oxfmt недоступний у PATH — формат-крок пропущено.\n')
|
|
154
|
+
return 0
|
|
155
|
+
}
|
|
156
|
+
const r = spawnSync(oxfmt, ['.'], { cwd, stdio: 'inherit', shell: false })
|
|
157
|
+
const code = typeof r.status === 'number' ? r.status : 1
|
|
158
|
+
if (code !== 0) log(`❌ lint: oxfmt — помилка (код ${code})\n`)
|
|
159
|
+
return code
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Scoped-режим (`lint <rule…>`): повний прогін НАЗВАНИХ правил — їх лінтер (`js/lint.mjs`,
|
|
164
|
+
* whole-repo) для тих, що його мають, + конформність для всіх названих. Дзеркалить `--full`,
|
|
165
|
+
* але звужено до правил, тож `lint ga` ≡ standalone `lint-ga`. Конформність-only правила
|
|
166
|
+
* (напр. `changelog` із hk) не мають `js/lint.mjs` → проганяється лише їх конформність
|
|
167
|
+
* (зворотна сумісність із колишнім `fix <rule>`). oxfmt у scoped НЕ запускається — це
|
|
168
|
+
* таргетований прогін правил, а не глобальне форматування.
|
|
169
|
+
* @param {string[]} rules id названих правил
|
|
170
|
+
* @param {{ cwd: string, readOnly: boolean, rulesDir: string, conformance: boolean, log: (s: string) => void }} ctx контекст (`conformance` — чи запускати конформність; false для юніт-тестів із кастомним rulesDir, де реальний пакет недоступний)
|
|
171
|
+
* @returns {Promise<number>} найгірший код (read-only — fail-fast на першому ненульовому)
|
|
172
|
+
*/
|
|
173
|
+
async function runScopedRules(rules, ctx) {
|
|
174
|
+
const { cwd, readOnly, rulesDir, conformance, log } = ctx
|
|
175
|
+
const metaById = readAllMeta(rulesDir)
|
|
176
|
+
const linterIds = rules.filter(id => existsSync(join(rulesDir, id, 'js', 'lint.mjs')))
|
|
177
|
+
let worst = 0
|
|
178
|
+
if (linterIds.length > 0) {
|
|
179
|
+
const perFile = await runPerFileRules(linterIds, { rulesDir, changed: undefined, cwd, readOnly, metaById, log })
|
|
180
|
+
if (perFile.stop) return perFile.code
|
|
181
|
+
worst = perFile.code
|
|
182
|
+
}
|
|
183
|
+
if (!conformance) return worst
|
|
184
|
+
const conformanceCode = await runConformance(cwd, readOnly, log, rules)
|
|
185
|
+
if (conformanceCode !== 0) {
|
|
186
|
+
if (readOnly) return conformanceCode
|
|
187
|
+
worst = conformanceCode
|
|
188
|
+
}
|
|
189
|
+
return worst
|
|
190
|
+
}
|
|
191
|
+
|
|
123
192
|
/**
|
|
124
193
|
* Запускає lint-оркестрацію.
|
|
125
194
|
* @param {{ full?: boolean, readOnly?: boolean, rules?: string[], cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
|
|
126
195
|
* - `full` — весь репо (`true`) проти дельти vs origin (`false`, default);
|
|
127
196
|
* - `readOnly` — лише детект без мутацій (`true`) проти fix (`false`, default);
|
|
128
|
-
* - `rules` — непорожній
|
|
197
|
+
* - `rules` — непорожній scope → повний прогін лише цих правил (лінтер + конформність, whole-repo).
|
|
129
198
|
* @returns {Promise<number>} exit code
|
|
130
199
|
*/
|
|
131
200
|
export async function runLint(opts = {}) {
|
|
@@ -136,9 +205,9 @@ export async function runLint(opts = {}) {
|
|
|
136
205
|
const rulesDir = opts.rulesDir ?? RULES_DIR
|
|
137
206
|
const log = opts.log ?? (s => process.stdout.write(s))
|
|
138
207
|
|
|
139
|
-
//
|
|
208
|
+
// Scoped режим (`lint <rule…>`): повний прогін названих правил — лінтер + конформність.
|
|
140
209
|
if (rules.length > 0) {
|
|
141
|
-
return
|
|
210
|
+
return runScopedRules(rules, { cwd, readOnly, rulesDir, conformance: opts.rulesDir === undefined, log })
|
|
142
211
|
}
|
|
143
212
|
|
|
144
213
|
// Default scope — дельта vs origin (merge-base main/origin/main); `--full` — весь репо.
|
|
@@ -149,7 +218,8 @@ export async function runLint(opts = {}) {
|
|
|
149
218
|
}
|
|
150
219
|
|
|
151
220
|
const metaById = readAllMeta(rulesDir)
|
|
152
|
-
const
|
|
221
|
+
const enabledRuleIds = await readEnabledLintRuleIds(metaById, cwd)
|
|
222
|
+
const ids = selectLintRules(metaById, full, enabledRuleIds)
|
|
153
223
|
const perFile = await runPerFileRules(ids, { rulesDir, changed, cwd, readOnly, metaById, log })
|
|
154
224
|
if (perFile.stop) return perFile.code
|
|
155
225
|
let worst = perFile.code
|
|
@@ -163,5 +233,12 @@ export async function runLint(opts = {}) {
|
|
|
163
233
|
worst = conformanceCode
|
|
164
234
|
}
|
|
165
235
|
}
|
|
236
|
+
|
|
237
|
+
// Формат-крок (oxfmt): fix-режим — завжди (будь-який scope); read-only пропускаємо (нуль
|
|
238
|
+
// мутацій). Кастомний rulesDir (юніт-тести) — реальний пакет недоступний, тож пропускаємо.
|
|
239
|
+
if (!readOnly && opts.rulesDir === undefined) {
|
|
240
|
+
const fmtCode = runFormat(cwd, log)
|
|
241
|
+
if (fmtCode !== 0) worst = fmtCode
|
|
242
|
+
}
|
|
166
243
|
return worst
|
|
167
244
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: lint.mjs
|
|
4
|
+
resource: npm/rules/php/js/lint.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 7de6b473
|
|
7
|
+
score: 100
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Оркестраторний адаптер правила `php` для `n-cursor lint`. Делегує лінтер-фазу наявній функції `run` із `../lint/lint.mjs` (composer audit, php-cs-fixer у `--dry-run`, phpstan/psalm). Режиму на рівні окремих файлів немає — composer-інструменти працюють по всьому проєкту, тож параметр `files` ігнорується. Інструменти read-only (`--dry-run`/`analyse`), тож мутацій робочого дерева немає.
|
|
11
|
+
|
|
12
|
+
## Поведінка
|
|
13
|
+
|
|
14
|
+
1. Викликає `run` — лінтер-фаза php по всьому проєкту.
|
|
15
|
+
2. Повертає код виходу інструменту.
|
|
16
|
+
|
|
17
|
+
## Гарантії поведінки
|
|
18
|
+
|
|
19
|
+
- Read-only: інструменти не мутують робоче дерево.
|
|
20
|
+
- Не звертається до мережі напряму (composer-кроки можуть, але це поведінка делегата, не цього модуля).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @see ./docs/lint.md */
|
|
2
|
+
import { run } from '../lint/lint.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Оркестраторний адаптер `n-cursor lint php` (лінтер-фаза): composer audit + php-cs-fixer
|
|
6
|
+
* (`--dry-run`) + phpstan/psalm через `run` (read-only — мутацій немає, тож `opts` ігнорується).
|
|
7
|
+
* Структурні php.mdc-перевірки — у конформність-фазі. Без composer-інструментів крок — no-op.
|
|
8
|
+
* @param {string[] | undefined} _files ігнорується (whole-repo обхід)
|
|
9
|
+
* @returns {number} exit code
|
|
10
|
+
*/
|
|
11
|
+
export function lint(_files) {
|
|
12
|
+
return run()
|
|
13
|
+
}
|
package/rules/php/php.mdc
CHANGED
|
@@ -63,9 +63,7 @@ composer audit
|
|
|
63
63
|
|
|
64
64
|
## lint-php
|
|
65
65
|
|
|
66
|
-
`composer`-інструмененти не мають єдиного CLI, який сам обходить репозиторій, тому
|
|
67
|
-
|
|
68
|
-
- Канон `package.json#scripts.lint-php` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
66
|
+
`composer`-інструмененти не мають єдиного CLI, який сам обходить репозиторій, тому php-лінт делегується у JS-скрипт-обгортку. Запуск — через **`n-cursor lint php`** (CI — `--read-only`); окремого `package.json`-скрипта немає.
|
|
69
67
|
|
|
70
68
|
Скрипт `run-php.mjs`:
|
|
71
69
|
|
|
@@ -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
|
+
}
|