@nitra/cursor 12.3.2 → 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.
- package/.claude-template/hooks/capture-decisions.sh +36 -19
- package/.claude-template/hooks/lib/tooling-only.sh +16 -0
- package/CHANGELOG.md +6 -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 +62 -3
- 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/rego/rego.mdc +2 -6
- 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/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/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
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`** — весь репо: усі правила (per-file лінтери + конформність) + `oxfmt` у кінці (fix-режим);
|
|
72
|
+
- **`n-cursor lint`** — дельта vs origin (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
|
+
Модуль відповідає за визначення та виконання процесу лінтування коду. Функція `selectLintRules` вибирає та сортує ідентифікатори правил лінтування на основі конфігурацій, визначених у meta.json. Функція `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 вибирає і сортує ідентифікатори правил для лінтування на основі їхніх конфігурацій, включаючи можливість включення правил, що застосовуються до всього репозиторію.
|
|
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` — непорожній
|
|
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
|
-
//
|
|
191
|
+
// Scoped режим (`lint <rule…>`): повний прогін названих правил — лінтер + конформність.
|
|
140
192
|
if (rules.length > 0) {
|
|
141
|
-
return
|
|
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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -3,324 +3,27 @@ type: JS Module
|
|
|
3
3
|
title: lint.mjs
|
|
4
4
|
resource: npm/rules/python/lint/lint.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 61a1e3c3
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 90
|
|
9
|
+
issues: internal-name:runStandardLint,judge:inaccurate:0.99
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
7
11
|
---
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
загального лінт-пайплайну монорепозиторію. Крок виконує перевірку Python-частини
|
|
11
|
-
проєкту відповідно до правила `python.mdc` і базується на пакетному менеджері
|
|
12
|
-
[uv](https://docs.astral.sh/uv/).
|
|
13
|
+
## Огляд
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Забезпечує виконання обов'язкових кроків для валідації Python-коду відповідно до правил, визначених у `python.mdc`, використовуючи інструменти з [uv](https://docs.astral.sh/uv/). Якщо `pyproject.toml` відсутній у корені, процес завершується з кодом 0. Якщо файл присутній, але `uv` не знайдено в PATH, це розглядається як помилка. Обов'язкові кроки включають перевірку актуальності lock-файлу (`uv lock --check`) та збірку середовища (`uv sync --frozen`). Опціональні лінтери (`ruff`, `mypy`) запускаються лише за умови їх доступності через `uv run`. Цей процес реалізує канон патерну `lint-*` (серіалізація через `runStandardLint`).
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
файлу `pyproject.toml`, крок завершується успіхом (`exit code 0`) без запуску
|
|
18
|
-
будь-яких інструментів. Це дозволяє безпечно вмикати крок у репозиторіях
|
|
19
|
-
без Python-частини.
|
|
20
|
-
- Якщо `pyproject.toml` присутній, але бінарника `uv` немає в `PATH`, крок
|
|
21
|
-
завершується помилкою. Інших пакет-менеджерів (Poetry, pip, pdm тощо) модуль
|
|
22
|
-
не підтримує — `uv` є єдиним каноном.
|
|
23
|
-
- Обовʼязкові кроки `uv lock --check` і `uv sync --frozen` запускаються завжди,
|
|
24
|
-
якщо `uv` доступний.
|
|
25
|
-
- Опційні лінтери (`ruff check --fix`, `ruff format`, `mypy`) запускаються
|
|
26
|
-
лише якщо вони доступні через `uv run --frozen <tool> --version`. Якщо
|
|
27
|
-
відповідного інструмента у uv-середовищі немає — крок пропускається з
|
|
28
|
-
pass-повідомленням (аналогічно «optional vendor-tools» у `php.mdc`).
|
|
29
|
-
- `ruff` працює в auto-fix-режимі (`--fix`, потім `format`), тобто може
|
|
30
|
-
мутувати робоче дерево, подібно до `markdownlint-cli2 --fix` у `lint-text`
|
|
31
|
-
чи `clippy --fix` у `lint-rust`.
|
|
32
|
-
- Серіалізація запусків CLI організована через `runStandardLint` (а не через
|
|
33
|
-
безпосередній `withLock`) — це відповідає канону патерну `lint-*`, описаному
|
|
34
|
-
в `.cursor/rules/scripts.mdc` (секція «Серіалізація важких CLI-команд»).
|
|
17
|
+
## Поведінка
|
|
35
18
|
|
|
36
|
-
|
|
19
|
+
runLintPythonSteps виконує обов'язкові кроки для Python-лінтування за правилом python.mdc на базі [uv](https://docs.astral.sh/uv/). Якщо `pyproject.toml` відсутній, кроки пропускаються. Якщо `uv` не знайдено, виникає помилка. Виконує перевірку актуальності lock-файлу (`uv lock --check`) та збірку середовища (`uv sync --frozen`). Опціонально запускає лінтери (`ruff`, `mypy`) через `uv run`, якщо вони доступні.
|
|
20
|
+
runLintPython серіалізує запуск кроків лінтування Python через механізм `runStandardLint` та повертає код виходу.
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
2. CLI-точкою входу — при запуску напряму (`isRunAsCli`) виконує
|
|
40
|
-
`runLintPython()` і виставляє `process.exitCode`.
|
|
22
|
+
## Публічний API
|
|
41
23
|
|
|
42
|
-
|
|
24
|
+
runLintPythonSteps — Виконує внутрішні етапи перевірки коду Python без блокування.
|
|
25
|
+
runLintPython — Виконує публічну команду перевірки коду Python, забезпечуючи унікальність виконання на основі стану Git-дерева та використовуючи механізм блокування.
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
| -------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
-
| `runLintPythonSteps` | `function` | Виконує внутрішні кроки `lint-python` (без зовнішнього локу). Призначений для повторного використання з обгортки `runStandardLint` та для тестування. |
|
|
47
|
-
| `runLintPython` | `() => Promise<number>` | Публічна CLI-форма: запускає `runLintPythonSteps` через `runStandardLint`, який бере глобальний лок `lint-python` і дедупає прогони за станом git-дерева. |
|
|
27
|
+
## Гарантії поведінки
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Side effect модуля верхнього рівня: якщо файл запущений як CLI
|
|
52
|
-
(`isRunAsCli(import.meta.url)` повертає `true`), на верхньому рівні
|
|
53
|
-
виконується `await runLintPython()` і результат записується у
|
|
54
|
-
`process.exitCode`.
|
|
55
|
-
|
|
56
|
-
## Функції
|
|
57
|
-
|
|
58
|
-
### `runTool(label, cmd, args, pass, fail)`
|
|
59
|
-
|
|
60
|
-
Внутрішня (не експортується) функція-обгортка над `child_process.spawnSync`,
|
|
61
|
-
яка запускає вказаний CLI-крок і репортить результат через колбеки репортера.
|
|
62
|
-
|
|
63
|
-
- Сигнатура: `runTool(label: string, cmd: string, args: string[], pass: (msg: string) => void, fail: (msg: string) => void): boolean`
|
|
64
|
-
- Параметри:
|
|
65
|
-
- `label` — людиночитана назва кроку, використовується у повідомленнях
|
|
66
|
-
(`lint-python: <label> — OK` / `lint-python: <label> — помилка ...`).
|
|
67
|
-
- `cmd` — абсолютний шлях до виконуваного файлу (наприклад, отриманий через
|
|
68
|
-
`resolveCmd('uv')`).
|
|
69
|
-
- `args` — масив аргументів CLI.
|
|
70
|
-
- `pass` — callback репортера для успіху.
|
|
71
|
-
- `fail` — callback репортера для невдачі.
|
|
72
|
-
- Повертає: `true`, якщо процес завершився з `status === 0`, інакше `false`.
|
|
73
|
-
- Спосіб запуску: `spawnSync(cmd, args, { stdio: 'inherit', shell: false })`.
|
|
74
|
-
Це означає, що stdout/stderr CLI-кроку успадковуються від батьківського
|
|
75
|
-
процесу (видно користувачу), а інтерпретація аргументів shell-ом
|
|
76
|
-
вимкнена — аргументи передаються «as is».
|
|
77
|
-
- Обробка статусу: якщо `r.status` не число (наприклад, процес був убитий
|
|
78
|
-
сигналом), у повідомлення про помилку підставляється `1`.
|
|
79
|
-
- Side effects: запуск зовнішнього процесу; запис у stdout/stderr батька;
|
|
80
|
-
виклик `pass` або `fail` репортера.
|
|
81
|
-
|
|
82
|
-
### `uvToolAvailable(uv, tool)`
|
|
83
|
-
|
|
84
|
-
Внутрішня (не експортується) перевірка наявності лінтера всередині
|
|
85
|
-
uv-середовища.
|
|
86
|
-
|
|
87
|
-
- Сигнатура: `uvToolAvailable(uv: string, tool: string): boolean`
|
|
88
|
-
- Параметри:
|
|
89
|
-
- `uv` — абсолютний шлях до бінарника `uv`.
|
|
90
|
-
- `tool` — назва бінарника, що перевіряється (`ruff`, `mypy`, тощо).
|
|
91
|
-
- Повертає: `true`, якщо `uv run --frozen <tool> --version` завершився з
|
|
92
|
-
кодом `0`, інакше `false`.
|
|
93
|
-
- Спосіб запуску: `spawnSync(uv, ['run', '--frozen', tool, '--version'],
|
|
94
|
-
{ stdio: 'ignore', shell: false })`. `stdio: 'ignore'` гасить весь вивід
|
|
95
|
-
пробної команди, щоб не засмічувати лог.
|
|
96
|
-
- Side effects: запуск дочірнього процесу `uv run --frozen <tool> --version`.
|
|
97
|
-
Опція `--frozen` гарантує, що `uv` не намагатиметься оновлювати lock-файл
|
|
98
|
-
під час перевірки.
|
|
99
|
-
|
|
100
|
-
### `runLintPythonSteps(cwd?)`
|
|
101
|
-
|
|
102
|
-
Експортована функція. Виконує всю послідовність кроків `lint-python` без
|
|
103
|
-
зовнішнього серіалізаційного локу.
|
|
104
|
-
|
|
105
|
-
- Сигнатура: `runLintPythonSteps(cwd?: string): number`
|
|
106
|
-
- Параметри:
|
|
107
|
-
- `cwd` — корінь репозиторію. За замовчуванням `process.cwd()`.
|
|
108
|
-
- Повертає: код виходу — `0`, якщо всі обовʼязкові кроки пройшли успішно,
|
|
109
|
-
`1` — якщо хоча б один крок зафейлив. Кінцевий код повертається через
|
|
110
|
-
`reporter.getExitCode()` (інстансу `createCheckReporter`).
|
|
111
|
-
- Алгоритм:
|
|
112
|
-
1. Створює репортер: `const reporter = createCheckReporter()`,
|
|
113
|
-
дістає колбеки `{ pass, fail }`.
|
|
114
|
-
2. Перевіряє `existsSync(join(cwd, 'pyproject.toml'))`. Якщо файла
|
|
115
|
-
немає — викликає `pass(...)` з повідомленням «кроки Python пропущено»
|
|
116
|
-
і повертає `reporter.getExitCode()`.
|
|
117
|
-
3. `const uv = resolveCmd('uv')` — резолвить абсолютний шлях до `uv`.
|
|
118
|
-
Якщо `uv` не знайдено — `fail(...)` і повернення коду.
|
|
119
|
-
4. Виконує `runTool('uv lock --check', uv, ['lock', '--check'], pass, fail)`.
|
|
120
|
-
За невдачі — повертає поточний код (далі не йде).
|
|
121
|
-
5. Виконує `runTool('uv sync --frozen', uv, ['sync', '--frozen'], pass,
|
|
122
|
-
fail)`. За невдачі — повертає поточний код.
|
|
123
|
-
6. Створює локальний хелпер `runOptionalUvTool(tool, label, args)`
|
|
124
|
-
(див. нижче) і послідовно запускає:
|
|
125
|
-
- `runOptionalUvTool('ruff', 'ruff check --fix', ['check', '--fix', '.'])`
|
|
126
|
-
- `runOptionalUvTool('ruff', 'ruff format', ['format', '.'])`
|
|
127
|
-
- `runOptionalUvTool('mypy', 'mypy', ['.'])`
|
|
128
|
-
За першої ж справжньої невдачі (повертає `false`) — повернення поточного
|
|
129
|
-
коду виходу.
|
|
130
|
-
7. Повертає `reporter.getExitCode()`.
|
|
131
|
-
- Side effects:
|
|
132
|
-
- Запуск зовнішніх процесів (`uv lock`, `uv sync`, `uv run ruff`,
|
|
133
|
-
`uv run mypy`).
|
|
134
|
-
- `ruff check --fix` та `ruff format` можуть **модифікувати файли
|
|
135
|
-
проєкту** (auto-fix Python-коду).
|
|
136
|
-
- `uv sync --frozen` може створювати або оновлювати `.venv` (з повним
|
|
137
|
-
дотриманням `uv.lock`).
|
|
138
|
-
- Запис у stdout/stderr через `stdio: 'inherit'`.
|
|
139
|
-
|
|
140
|
-
### `runOptionalUvTool(tool, label, args)` (вкладена у `runLintPythonSteps`)
|
|
141
|
-
|
|
142
|
-
Внутрішній замикач, доступний лише всередині `runLintPythonSteps`. Захоплює
|
|
143
|
-
`uv`, `pass`, `fail` із зовнішньої області видимості.
|
|
144
|
-
|
|
145
|
-
- Сигнатура: `runOptionalUvTool(tool: string, label: string, args: string[]): boolean`
|
|
146
|
-
- Параметри:
|
|
147
|
-
- `tool` — імʼя інструмента (`ruff`, `mypy`).
|
|
148
|
-
- `label` — назва кроку для повідомлень.
|
|
149
|
-
- `args` — аргументи, які слід передати інструменту після `uv run --frozen <tool>`.
|
|
150
|
-
- Повертає: `true`, якщо крок успішно завершився **або** інструмент
|
|
151
|
-
відсутній у uv-середовищі (тоді крок пропускається з pass-повідомленням).
|
|
152
|
-
`false` повертається тільки коли інструмент доступний і завершився з
|
|
153
|
-
ненульовим статусом.
|
|
154
|
-
- Логіка:
|
|
155
|
-
1. `if (!uvToolAvailable(uv, tool))` → `pass(...)` з повідомленням «крок
|
|
156
|
-
пропущено» і повертає `true` (це коректне продовження пайплайну,
|
|
157
|
-
інструмент трактується як optional).
|
|
158
|
-
2. Інакше викликає `runTool(label, uv, ['run', '--frozen', tool, ...args],
|
|
159
|
-
pass, fail)`.
|
|
160
|
-
- Side effects: ті ж, що й у `runTool` / `uvToolAvailable` (запуск дочірніх
|
|
161
|
-
процесів, оновлення репортера).
|
|
162
|
-
|
|
163
|
-
### `runLintPython`
|
|
164
|
-
|
|
165
|
-
Публічна обгортка-стрілкова функція.
|
|
166
|
-
|
|
167
|
-
- Сигнатура: `runLintPython(): Promise<number>`
|
|
168
|
-
- Параметри: немає.
|
|
169
|
-
- Повертає: `Promise<number>` — код виходу, отриманий з `runStandardLint`.
|
|
170
|
-
- Реалізація: `runStandardLint(import.meta.dirname, runLintPythonSteps)`.
|
|
171
|
-
Сенс параметрів:
|
|
172
|
-
- `import.meta.dirname` — директорія самого модуля; використовується
|
|
173
|
-
`runStandardLint` як ідентифікатор для дедуплікації / стану git-дерева.
|
|
174
|
-
- `runLintPythonSteps` — функція кроків, яку `runStandardLint` викличе
|
|
175
|
-
всередині глобального локу `lint-python`.
|
|
176
|
-
- Серіалізація: `runStandardLint` бере глобальний лок `lint-python` (як
|
|
177
|
-
описано в `scripts.mdc`) та дедупає прогони за станом git-дерева, тому
|
|
178
|
-
паралельні виклики `runLintPython()` не перетинатимуться по запуску
|
|
179
|
-
`uv`.
|
|
180
|
-
- Side effects: ті самі, що й у `runLintPythonSteps`, плюс блокування на
|
|
181
|
-
файловому локу.
|
|
182
|
-
|
|
183
|
-
## CLI-вхід (верхній рівень модуля)
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
if (isRunAsCli(import.meta.url)) {
|
|
187
|
-
process.exitCode = await runLintPython()
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
- Перевірка `isRunAsCli(import.meta.url)` встановлює, чи запущений файл
|
|
192
|
-
безпосередньо як CLI-точка входу (наприклад, `node lint.mjs` або через
|
|
193
|
-
`n-cursor`), а не імпортований як модуль.
|
|
194
|
-
- Якщо так — виконується top-level `await runLintPython()`, а результат
|
|
195
|
-
кладеться у `process.exitCode`. Це означає, що Node завершиться з цим
|
|
196
|
-
кодом після того, як event loop спорожніє.
|
|
197
|
-
- Якщо файл імпортовано як модуль, цей блок не виконується — викликач сам
|
|
198
|
-
вирішує, як використати експортовані функції.
|
|
199
|
-
|
|
200
|
-
## Залежності
|
|
201
|
-
|
|
202
|
-
### Стандартна бібліотека Node.js
|
|
203
|
-
|
|
204
|
-
- `node:child_process` → `spawnSync` — синхронний запуск зовнішніх процесів
|
|
205
|
-
(`uv`, `uv run …`).
|
|
206
|
-
- `node:fs` → `existsSync` — перевірка наявності `pyproject.toml`.
|
|
207
|
-
- `node:path` → `join` — побудова повного шляху до `pyproject.toml` від `cwd`.
|
|
208
|
-
|
|
209
|
-
### Внутрішні модулі репозиторію
|
|
210
|
-
|
|
211
|
-
- `../../../scripts/cli-entry.mjs` → `isRunAsCli` — детекція CLI-режиму
|
|
212
|
-
через `import.meta.url`.
|
|
213
|
-
- `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` —
|
|
214
|
-
фабрика репортера з методами `pass`, `fail`, `getExitCode`. Цей патерн
|
|
215
|
-
єдиний для всіх лінт-кроків.
|
|
216
|
-
- `../../../scripts/utils/resolve-cmd.mjs` → `resolveCmd` — пошук
|
|
217
|
-
виконуваного файлу в `PATH` (повертає абсолютний шлях або `null`).
|
|
218
|
-
- `../../../scripts/lib/run-standard-lint.mjs` → `runStandardLint` —
|
|
219
|
-
стандартизована обгортка над лінт-кроком (глобальний лок + дедуплікація
|
|
220
|
-
за станом git-дерева).
|
|
221
|
-
|
|
222
|
-
### Зовнішні бінарники (runtime-залежності)
|
|
223
|
-
|
|
224
|
-
- `uv` — обовʼязковий у `PATH`, якщо в репозиторії є `pyproject.toml`.
|
|
225
|
-
- `ruff` — опційний, перевіряється через `uv run --frozen ruff --version`.
|
|
226
|
-
- `mypy` — опційний, перевіряється через `uv run --frozen mypy --version`.
|
|
227
|
-
|
|
228
|
-
### Артефакти у проєкті
|
|
229
|
-
|
|
230
|
-
- `pyproject.toml` (у корені `cwd`) — тригер запуску Python-частини.
|
|
231
|
-
- `uv.lock` — використовується `uv lock --check` та `uv sync --frozen`,
|
|
232
|
-
має бути актуальним.
|
|
233
|
-
|
|
234
|
-
## Потік виконання / Використання
|
|
235
|
-
|
|
236
|
-
### Сценарій 1: Python-частини немає
|
|
237
|
-
|
|
238
|
-
1. `runLintPython()` → `runStandardLint(...)` → `runLintPythonSteps()`.
|
|
239
|
-
2. `existsSync('<cwd>/pyproject.toml')` повертає `false`.
|
|
240
|
-
3. Репортер фіксує pass-повідомлення «немає pyproject.toml у корені — кроки
|
|
241
|
-
Python пропущено».
|
|
242
|
-
4. Повертається `0`.
|
|
243
|
-
|
|
244
|
-
### Сценарій 2: Python є, але `uv` не встановлений
|
|
245
|
-
|
|
246
|
-
1. `existsSync('pyproject.toml')` → `true`.
|
|
247
|
-
2. `resolveCmd('uv')` → `null`.
|
|
248
|
-
3. `fail('lint-python: `uv` не знайдено в PATH ...')`.
|
|
249
|
-
4. Повертається `1`.
|
|
250
|
-
|
|
251
|
-
### Сценарій 3: Повний прогон з усіма лінтерами
|
|
252
|
-
|
|
253
|
-
1. `uv lock --check` — перевірка lock-файлу. За невдачі вихід `1`.
|
|
254
|
-
2. `uv sync --frozen` — інсталяція середовища строго за `uv.lock`. За
|
|
255
|
-
невдачі вихід `1`.
|
|
256
|
-
3. `uvToolAvailable(uv, 'ruff')` → `true` → `uv run --frozen ruff check
|
|
257
|
-
--fix .`. Може **змінити файли**.
|
|
258
|
-
4. `uv run --frozen ruff format .`. Також може **змінити файли**.
|
|
259
|
-
5. `uvToolAvailable(uv, 'mypy')` → `true` → `uv run --frozen mypy .`.
|
|
260
|
-
Лише читає, не змінює дерево.
|
|
261
|
-
6. Якщо всі кроки повернули `0` — підсумок `0`. Інакше — перший
|
|
262
|
-
ненульовий код розриває послідовність і повертається.
|
|
263
|
-
|
|
264
|
-
### Сценарій 4: `ruff` або `mypy` не встановлені у uv-середовищі
|
|
265
|
-
|
|
266
|
-
- Для відповідного інструмента `uvToolAvailable` поверне `false`.
|
|
267
|
-
- Виводиться pass-повідомлення «<tool> недоступний у uv-середовищі —
|
|
268
|
-
крок пропущено».
|
|
269
|
-
- Інші кроки виконуються штатно.
|
|
270
|
-
|
|
271
|
-
### Як викликати з коду
|
|
272
|
-
|
|
273
|
-
```js
|
|
274
|
-
import { runLintPython, runLintPythonSteps } from './lint.mjs'
|
|
275
|
-
|
|
276
|
-
// Стандартний шлях: з локом, дедуплікацією, асинхронно.
|
|
277
|
-
const code = await runLintPython()
|
|
278
|
-
process.exit(code)
|
|
279
|
-
|
|
280
|
-
// Прямий виклик без локу (наприклад, у тестах або з власною серіалізацією):
|
|
281
|
-
const codeRaw = runLintPythonSteps('/path/to/repo')
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Як викликати з CLI
|
|
285
|
-
|
|
286
|
-
Файл є виконуваною точкою входу для лінт-пайплайну. У звичайному монорепо
|
|
287
|
-
він викликається через спільний раннер (`n-cursor`, `bun run lint` тощо).
|
|
288
|
-
Прямий запуск:
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
node npm/rules/python/lint/lint.mjs
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Кодом виходу буде число з `runLintPython()` (`0` — OK, `1` — є помилки).
|
|
295
|
-
|
|
296
|
-
## Rebuild Test
|
|
297
|
-
|
|
298
|
-
За цією документацією можна відтворити модуль так:
|
|
299
|
-
|
|
300
|
-
1. Створити ES Module-файл, що імпортує `spawnSync` з `node:child_process`,
|
|
301
|
-
`existsSync` з `node:fs`, `join` з `node:path`, а також `isRunAsCli`,
|
|
302
|
-
`createCheckReporter`, `resolveCmd`, `runStandardLint` з відповідних
|
|
303
|
-
шляхів `../../../scripts/...`.
|
|
304
|
-
2. Реалізувати приватну `runTool(label, cmd, args, pass, fail)`:
|
|
305
|
-
`spawnSync` з `stdio: 'inherit'`, `shell: false`; при `status === 0`
|
|
306
|
-
викликати `pass`, інакше `fail` з кодом (типу number або `1` при
|
|
307
|
-
неприродному завершенні); повертати `boolean`.
|
|
308
|
-
3. Реалізувати `uvToolAvailable(uv, tool)`: `spawnSync(uv, ['run',
|
|
309
|
-
'--frozen', tool, '--version'], { stdio: 'ignore', shell: false })` →
|
|
310
|
-
`r.status === 0`.
|
|
311
|
-
4. Експортувати `runLintPythonSteps(cwd = process.cwd())`:
|
|
312
|
-
- створити репортер;
|
|
313
|
-
- якщо `pyproject.toml` відсутній → `pass(...)` і повернути код;
|
|
314
|
-
- резолвити `uv`; якщо немає → `fail(...)` і повернути;
|
|
315
|
-
- послідовно: `uv lock --check`, `uv sync --frozen` (обовʼязкові);
|
|
316
|
-
- опційні через локальну функцію-замикач `runOptionalUvTool`: `ruff
|
|
317
|
-
check --fix .`, `ruff format .`, `mypy .` — кожен через
|
|
318
|
-
`uvToolAvailable` + `runTool`;
|
|
319
|
-
- повернути `reporter.getExitCode()`.
|
|
320
|
-
5. Експортувати `runLintPython = () => runStandardLint(import.meta.dirname,
|
|
321
|
-
runLintPythonSteps)`.
|
|
322
|
-
6. На верхньому рівні: `if (isRunAsCli(import.meta.url)) process.exitCode =
|
|
323
|
-
await runLintPython()`.
|
|
324
|
-
|
|
325
|
-
Результат повинен поведінково збігтися з оригіналом: ті самі повідомлення,
|
|
326
|
-
ті самі коди виходу, така ж серіалізація та обробка опційних інструментів.
|
|
29
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -63,9 +63,11 @@ function uvToolAvailable(uv, tool) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Внутрішні кроки `lint-python` без локу.
|
|
65
65
|
* @param {string} [cwd] корінь репозиторію
|
|
66
|
+
* @param {{ readOnly?: boolean }} [opts] readOnly → `ruff` без `--fix`, `ruff format --check` (нуль мутацій, CI/детект)
|
|
66
67
|
* @returns {number} 0 — OK, 1 — є помилки
|
|
67
68
|
*/
|
|
68
|
-
export function runLintPythonSteps(cwd = process.cwd()) {
|
|
69
|
+
export function runLintPythonSteps(cwd = process.cwd(), opts = {}) {
|
|
70
|
+
const readOnly = opts.readOnly === true
|
|
69
71
|
const reporter = createCheckReporter()
|
|
70
72
|
const { pass, fail } = reporter
|
|
71
73
|
|
|
@@ -98,8 +100,10 @@ export function runLintPythonSteps(cwd = process.cwd()) {
|
|
|
98
100
|
return runTool(label, uv, ['run', '--frozen', tool, ...args], pass, fail)
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
const ruffCheck = readOnly ? ['check', '.'] : ['check', '--fix', '.']
|
|
104
|
+
const ruffFormat = readOnly ? ['format', '--check', '.'] : ['format', '.']
|
|
105
|
+
if (!runOptionalUvTool('ruff', readOnly ? 'ruff check' : 'ruff check --fix', ruffCheck)) return reporter.getExitCode()
|
|
106
|
+
if (!runOptionalUvTool('ruff', readOnly ? 'ruff format --check' : 'ruff format', ruffFormat)) return reporter.getExitCode()
|
|
103
107
|
if (!runOptionalUvTool('mypy', 'mypy', ['.'])) return reporter.getExitCode()
|
|
104
108
|
|
|
105
109
|
return reporter.getExitCode()
|
|
@@ -107,10 +111,12 @@ export function runLintPythonSteps(cwd = process.cwd()) {
|
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
113
|
* Публічна CLI-форма: серіалізує через `withLock('lint-python')` + дедуп за станом git-дерева.
|
|
114
|
+
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без мутацій (проброс у кроки)
|
|
110
115
|
* @returns {Promise<number>} код виходу
|
|
111
116
|
*/
|
|
112
|
-
export const runLintPython = () =>
|
|
117
|
+
export const runLintPython = (opts = {}) =>
|
|
118
|
+
runStandardLint(import.meta.dirname, () => runLintPythonSteps(process.cwd(), opts))
|
|
113
119
|
|
|
114
120
|
if (isRunAsCli(import.meta.url)) {
|
|
115
|
-
process.exitCode = await runLintPython()
|
|
121
|
+
process.exitCode = await runLintPython({ readOnly: process.argv.includes('--read-only') })
|
|
116
122
|
}
|
package/rules/python/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": "pyproject.toml" } }
|
|
1
|
+
{ "auto": { "glob": "pyproject.toml" }, "lint": "full" }
|
package/rules/rego/rego.mdc
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Opa, Rego — інструментарій (VS Code,
|
|
2
|
+
description: Opa, Rego — інструментарій (VS Code, opa/regal)
|
|
3
3
|
version: '1.1'
|
|
4
4
|
globs: "**/*.rego"
|
|
5
5
|
alwaysApply: false
|
|
@@ -12,17 +12,13 @@ alwaysApply: false
|
|
|
12
12
|
## Перевірка
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
15
|
+
n-cursor lint rego
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Цілі — `npm/rules/` (рекурсивно знаходить `.rego` у `<rule>/policy/<concern>/`). Інші *.rego поза деревом додай у `LINT_TARGETS` у `npm/rules/rego/lint/lint.mjs`.
|
|
19
19
|
|
|
20
20
|
`opa` і `regal` — лише у `PATH`, **не** додавай у `dependencies` / `devDependencies`.
|
|
21
21
|
|
|
22
|
-
### `package.json`
|
|
23
|
-
|
|
24
|
-
- Канон `scripts.lint-rego`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
25
|
-
|
|
26
22
|
### `.vscode/extensions.json`
|
|
27
23
|
|
|
28
24
|
- Канон `recommendations` має містити `tsandall.opa` (LSP, format-on-save через `opa fmt`): [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
@@ -5,14 +5,6 @@ package security.package_json
|
|
|
5
5
|
|
|
6
6
|
import rego.v1
|
|
7
7
|
|
|
8
|
-
# ── deny: кожен snippet leaf має співпадати з input ──────────────────────────
|
|
9
|
-
deny contains msg if {
|
|
10
|
-
some script_name, expected in data.template.snippet.scripts
|
|
11
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
12
|
-
actual != expected
|
|
13
|
-
msg := sprintf("package.json: scripts.%s має бути %q (security.mdc)", [script_name, expected])
|
|
14
|
-
}
|
|
15
|
-
|
|
16
8
|
# ── deny: жодного ключа з deny у dependencies/devDependencies ────────────────
|
|
17
9
|
deny contains msg if {
|
|
18
10
|
some pkg, reason in data.template.deny.dependencies
|
|
@@ -25,14 +17,3 @@ deny contains msg if {
|
|
|
25
17
|
pkg in object.keys(object.get(input, "devDependencies", {}))
|
|
26
18
|
msg := sprintf("package.json: devDependencies.%s — %s (security.mdc)", [pkg, reason])
|
|
27
19
|
}
|
|
28
|
-
|
|
29
|
-
# ── deny: рядкові поля з contains мають містити кожен substring ──────────────
|
|
30
|
-
# Перевіряємо лише наявні поля (якщо `scripts.<name>` відсутній — поле опціональне).
|
|
31
|
-
deny contains msg if {
|
|
32
|
-
some script_name, needles in data.template.contains.scripts
|
|
33
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
34
|
-
actual != ""
|
|
35
|
-
some needle in needles
|
|
36
|
-
not contains(actual, needle)
|
|
37
|
-
msg := sprintf("package.json: scripts.%s має містити %q (security.mdc)", [script_name, needle])
|
|
38
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Локальний та CI-секюріті-лінт через TruffleHog —
|
|
2
|
+
description: Локальний та CI-секюріті-лінт через TruffleHog — `.trufflehog-exclude`, канонічний placeholder `sample-secret` у прикладних файлах
|
|
3
3
|
globs: "**/.trufflehog-exclude,**/package.json,**/.github/workflows/**/*.yml"
|
|
4
4
|
alwaysApply: false
|
|
5
5
|
version: '2.1'
|
|
@@ -7,10 +7,10 @@ version: '2.1'
|
|
|
7
7
|
|
|
8
8
|
[TruffleHog](https://github.com/trufflesecurity/trufflehog) — глобальний CLI (як `shellcheck`, `conftest`); **не** додавай до `dependencies`/`devDependencies`.
|
|
9
9
|
|
|
10
|
-
## Канон `package.json
|
|
10
|
+
## Канон `package.json`
|
|
11
|
+
|
|
12
|
+
Скан запускається через `n-cursor lint security` (CI — `n-cursor lint security --read-only`); окремого `lint-*` скрипта в `package.json` немає.
|
|
11
13
|
|
|
12
|
-
- `lint-security` скрипт: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
13
|
-
- `lint` агрегатор повинен містити: [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
14
14
|
- Заборонено `trufflehog` у `dependencies`/`devDependencies`: [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
15
15
|
|
|
16
16
|
**Зауваження:**
|
|
@@ -19,8 +19,7 @@ version: '2.1'
|
|
|
19
19
|
- `--no-update` — вимикає self-update check (CI-friendly).
|
|
20
20
|
- `--exclude-paths .trufflehog-exclude` — файл з regex-patterns, які треба пропускати (аналог `[allowlist].paths` із gitleaks).
|
|
21
21
|
- `--results=verified,unknown` — показує лише верифіковані секрети + ті, що TruffleHog не зміг перевірити (`unverified` дублікат відсіюється).
|
|
22
|
-
- `--fail` — exit-code `183` за наявності знахідок (
|
|
23
|
-
- Позиція в `lint`: за конвенцією після інших `lint-*` і перед `oxfmt`.
|
|
22
|
+
- `--fail` — exit-code `183` за наявності знахідок (щоб лінт падав).
|
|
24
23
|
|
|
25
24
|
## `.trufflehog-exclude` (рекомендована основа)
|
|
26
25
|
|
|
@@ -10,16 +10,6 @@ package style_lint.package_json
|
|
|
10
10
|
|
|
11
11
|
import rego.v1
|
|
12
12
|
|
|
13
|
-
# ── deny: substring requirements у scripts (contains) ────────────────────
|
|
14
|
-
|
|
15
|
-
deny contains msg if {
|
|
16
|
-
some script_name, needles in data.template.contains.scripts
|
|
17
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
18
|
-
some needle in needles
|
|
19
|
-
not contains(actual, needle)
|
|
20
|
-
msg := sprintf("package.json: scripts.%s має містити %q (style-lint.mdc)", [script_name, needle])
|
|
21
|
-
}
|
|
22
|
-
|
|
23
13
|
# ── deny: 2-level snippet walker (для stylelint.extends, якщо поле є) ────
|
|
24
14
|
|
|
25
15
|
deny contains msg if {
|
|
@@ -95,16 +95,17 @@ $white-a1: color.adjust(white, $alpha: -0.85);
|
|
|
95
95
|
|
|
96
96
|
- **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
|
|
97
97
|
- **Форматування** узгоджуй з **`n-text.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
|
|
98
|
-
- **Запуск stylelint:** лише
|
|
98
|
+
- **Запуск stylelint:** лише через **`n-cursor lint style-lint`** (локально — з auto-fix; у CI — `--read-only`, нуль мутацій). Під капотом — `npx stylelint`; **не** використовуй **`bunx stylelint`**. Після змін запускай **`n-cursor lint style-lint`** і виправляй усе, що лишилось після auto-fix; за потреби — повний прогін `n-cursor lint --full`.
|
|
99
99
|
- **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
|
|
100
100
|
|
|
101
101
|
## Канон
|
|
102
102
|
|
|
103
103
|
### `package.json`
|
|
104
104
|
|
|
105
|
-
- `lint-style` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
106
105
|
- `stylelint.extends`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
107
106
|
|
|
107
|
+
Окремого `lint-style` скрипта немає — запуск через **`n-cursor lint style-lint`** (CI — `--read-only`).
|
|
108
|
+
|
|
108
109
|
### `.vscode/extensions.json`
|
|
109
110
|
|
|
110
111
|
- Канон `recommendations`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
@@ -120,9 +121,6 @@ $white-a1: color.adjust(white, $alpha: -0.85);
|
|
|
120
121
|
**`package.json`:**
|
|
121
122
|
|
|
122
123
|
```json title="package.json"
|
|
123
|
-
"scripts": {
|
|
124
|
-
"lint-style": "npx stylelint '**/*.{css,scss,vue}' --fix",
|
|
125
|
-
},
|
|
126
124
|
"devDependencies": {
|
|
127
125
|
"@nitra/stylelint-config": "^1.4.0"
|
|
128
126
|
},
|
|
@@ -131,7 +129,7 @@ $white-a1: color.adjust(white, $alpha: -0.85);
|
|
|
131
129
|
},
|
|
132
130
|
```
|
|
133
131
|
|
|
134
|
-
Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі `
|
|
132
|
+
Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі `n-cursor lint style-lint --read-only` у кроці **`run`**. **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
|
|
135
133
|
|
|
136
134
|
```yaml title=".github/workflows/lint-style.yml"
|
|
137
135
|
name: StyleLint
|
|
@@ -94,25 +94,20 @@ function checkTextConfigsExistence(passFn, failFn, cwd) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Перевіряє
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* `@nitra/*` гейт) — у Rego (`text.package_json`, `bun.package_json`).
|
|
97
|
+
* Перевіряє CI-workflow текстового стека: крок `n-cursor lint text --read-only` у
|
|
98
|
+
* `.github/workflows/lint-text.yml` (CI — нуль мутацій). Окремого `lint-text` скрипта в
|
|
99
|
+
* `package.json` немає — лінт через `n-cursor lint text`. Решта package.json-перевірок
|
|
100
|
+
* (Prettier-заборона, `@nitra/cspell-dict`, `@nitra/*` гейт) — у Rego (`text.package_json`, `bun.package_json`).
|
|
101
101
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
102
102
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
103
103
|
* @param {string} cwd корінь репозиторію
|
|
104
104
|
*/
|
|
105
|
-
async function
|
|
106
|
-
const pkgPath = join(cwd, 'package.json')
|
|
107
|
-
if (!existsSync(pkgPath)) return
|
|
108
|
-
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
109
|
-
checkLintTextScript(pkg.scripts?.['lint-text'], passFn, failFn)
|
|
110
|
-
|
|
105
|
+
async function checkLintTextWorkflow(passFn, failFn, cwd) {
|
|
111
106
|
const lintTextWf = join(cwd, '.github/workflows/lint-text.yml')
|
|
112
107
|
if (existsSync(lintTextWf)) {
|
|
113
108
|
const wf = await readFile(lintTextWf, 'utf8')
|
|
114
109
|
const root = parseWorkflowYaml(wf)
|
|
115
|
-
const canonRun = 'n-cursor lint
|
|
110
|
+
const canonRun = 'n-cursor lint text --read-only'
|
|
116
111
|
const ok = root ? anyRunStepIncludes(root, canonRun) : wf.includes(canonRun)
|
|
117
112
|
if (ok) {
|
|
118
113
|
passFn(`lint-text.yml викликає ${canonRun}`)
|
|
@@ -124,25 +119,6 @@ async function checkPackageJsonText(passFn, failFn, cwd) {
|
|
|
124
119
|
}
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
/**
|
|
128
|
-
* Перевіряє скрипт lint-text: канонічний — `n-cursor lint-text` (CLI пакета `@nitra/cursor` робить
|
|
129
|
-
* `cspell` → `runShellcheckText()` → `bunx markdownlint-cli2 --fix` → `runV8rWithGlobs()`).
|
|
130
|
-
* Дозволено whitespace навколо команди.
|
|
131
|
-
* @param {unknown} lintText значення `scripts.lint-text` з package.json
|
|
132
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
133
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
134
|
-
*/
|
|
135
|
-
function checkLintTextScript(lintText, passFn, failFn) {
|
|
136
|
-
const lt = typeof lintText === 'string' ? lintText.trim() : ''
|
|
137
|
-
if (lt === 'n-cursor lint-text') {
|
|
138
|
-
passFn('lint-text делегує CLI n-cursor lint-text (cspell + shellcheck + markdownlint + v8r)')
|
|
139
|
-
} else {
|
|
140
|
-
failFn(
|
|
141
|
-
'package.json: lint-text має бути "n-cursor lint-text" — CLI пакета @nitra/cursor виконує cspell → shellcheck → markdownlint-cli2 → v8r (text.mdc)'
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
122
|
/**
|
|
147
123
|
* Перевіряє відповідність проєкту правилам text.mdc.
|
|
148
124
|
* @param {string} [cwd] корінь репозиторію
|
|
@@ -166,7 +142,7 @@ export async function check(cwd = process.cwd()) {
|
|
|
166
142
|
}
|
|
167
143
|
}
|
|
168
144
|
|
|
169
|
-
await
|
|
145
|
+
await checkLintTextWorkflow(pass, fail, cwd)
|
|
170
146
|
|
|
171
147
|
return reporter.getExitCode()
|
|
172
148
|
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Перевірка кореневого `package.json` для GitHub Actions tooling (ga.mdc).
|
|
2
|
-
#
|
|
3
|
-
# Канон надходить через --data: { "template": { "contains": ... } }
|
|
4
|
-
# Структура --data сформована з template/package.json.contains.json.
|
|
5
|
-
#
|
|
6
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
7
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
8
|
-
package ga.package_json
|
|
9
|
-
|
|
10
|
-
import rego.v1
|
|
11
|
-
|
|
12
|
-
# Кожне рядкове поле з contains має містити кожен substring.
|
|
13
|
-
# Відсутність ключа → `""` → contains() = false → deny.
|
|
14
|
-
deny contains msg if {
|
|
15
|
-
some script_name, needles in data.template.contains.scripts
|
|
16
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
17
|
-
some needle in needles
|
|
18
|
-
not contains(actual, needle)
|
|
19
|
-
msg := sprintf("package.json: scripts.%s має містити %q (ga.mdc)", [script_name, needle])
|
|
20
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "scripts": { "lint-ga": ["n-cursor lint-ga"] } }
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Перевірка `package.json` для rego (rego.mdc).
|
|
2
|
-
#
|
|
3
|
-
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
-
# Структура --data сформована з template/package.json.snippet.json.
|
|
5
|
-
# Дозволяємо whitespace навколо значення (trim_space) — допуск, який мав
|
|
6
|
-
# попередній inline-варіант.
|
|
7
|
-
package rego.package_json
|
|
8
|
-
|
|
9
|
-
import rego.v1
|
|
10
|
-
|
|
11
|
-
deny contains msg if {
|
|
12
|
-
some script_name, expected in data.template.snippet.scripts
|
|
13
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
14
|
-
trim_space(actual) != expected
|
|
15
|
-
msg := sprintf("package.json: scripts.%s має бути %q (rego.mdc)", [script_name, expected])
|
|
16
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "scripts": { "lint-rego": "n-cursor lint-rego" } }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "scripts": { "lint": ["bun run lint-security"] } }
|