@nitra/cursor 12.8.1 → 12.8.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/CHANGELOG.md +12 -0
- package/bin/n-cursor.js +1 -1
- package/package.json +1 -1
- package/rules/feedback/feedback.mdc +3 -3
- package/rules/ga/ga.mdc +1 -1
- package/rules/ga/policy/workflow_common/workflow_common.rego +3 -3
- package/rules/js/coverage/coverage.mjs +7 -7
- package/rules/js/docs/fix.md +1 -1
- package/rules/js/docs/index.md +3 -3
- package/rules/js/docs/main.md +1 -1
- package/rules/js/js/check.mjs +10 -10
- package/rules/js/js/docs/check.md +3 -3
- package/rules/js/js/docs/index.md +3 -3
- package/rules/js/js/docs/lint-findings.md +1 -1
- package/rules/js/js/docs/tooling.md +1 -1
- package/rules/js/js/docs/utils_imports.md +8 -8
- package/rules/js/js/tooling.mjs +1 -1
- package/rules/js/js/utils_imports.mjs +3 -3
- package/rules/js/js.mdc +30 -6
- package/rules/js/main.mjs +22 -5
- package/rules/js/policy/jscpd/jscpd.rego +4 -4
- package/rules/js/policy/jscpd/target.json +1 -1
- package/rules/js/policy/lint_js_yml/lint_js_yml.rego +6 -6
- package/rules/js/policy/lint_js_yml/template/lint-js.yml.snippet.yml +1 -1
- package/rules/js/policy/package_json/package_json.rego +7 -7
- package/rules/js/policy/vscode_extensions/target.json +1 -1
- package/rules/js/policy/vscode_extensions/vscode_extensions.rego +2 -2
- package/rules/js-run/lib/docs/conn-file-rules.md +1 -1
- package/rules/{js-lint-ci → style}/docs/fix.md +1 -1
- package/rules/{js-lint-ci → style}/docs/index.md +3 -3
- package/rules/{style-lint → style}/docs/main.md +1 -1
- package/rules/{style-lint → style}/js/docs/index.md +3 -3
- package/rules/{style-lint → style}/js/docs/tooling.md +3 -3
- package/rules/{style-lint → style}/js/tooling.mjs +2 -2
- package/rules/{style-lint → style}/policy/lint_style_yml/lint_style_yml.rego +2 -2
- package/rules/{style-lint → style}/policy/lint_style_yml/template/lint-style.yml.snippet.yml +1 -1
- package/rules/{style-lint → style}/policy/package_json/package_json.rego +3 -3
- package/rules/{style-lint → style}/policy/vscode_extensions/vscode_extensions.rego +2 -2
- package/rules/{style-lint → style}/policy/vscode_settings/vscode_settings.rego +2 -2
- package/rules/{style-lint/style-lint.mdc → style/style.mdc} +3 -3
- package/rules/test/js/docs/stryker_config.md +1 -1
- package/rules/test/js/stryker_config.mjs +4 -4
- package/rules/test/js/vitest-config-pool-forks.mjs +1 -1
- package/rules/test/test.mdc +4 -4
- package/scripts/lib/docs/discover-checkable-rules.md +2 -2
- package/scripts/lib/docs/gha-workflow.md +1 -1
- package/scripts/lib/gha-workflow.mjs +1 -1
- package/scripts/lib/timing-summary.mjs +1 -1
- package/scripts/sync-setup-bun-deps-action.mjs +1 -1
- package/scripts/utils/resolve-js-root.mjs +1 -1
- package/skills/coverage-fix/meta.json +1 -1
- package/skills/lint/SKILL.md +2 -2
- package/skills/llm-patch/SKILL.md +1 -1
- package/rules/js-lint-ci/docs/main.md +0 -27
- package/rules/js-lint-ci/js/docs/index.md +0 -11
- package/rules/js-lint-ci/js-lint-ci.mdc +0 -45
- package/rules/js-lint-ci/main.mjs +0 -33
- package/rules/js-lint-ci/meta.json +0 -1
- package/rules/style-lint/docs/fix.md +0 -28
- package/rules/style-lint/docs/index.md +0 -12
- /package/rules/{style-lint → style}/main.mjs +0 -0
- /package/rules/{style-lint → style}/meta.json +0 -0
- /package/rules/{style-lint → style}/policy/lint_style_yml/target.json +0 -0
- /package/rules/{style-lint → style}/policy/package_json/target.json +0 -0
- /package/rules/{style-lint → style}/policy/package_json/template/package.json.snippet.json +0 -0
- /package/rules/{style-lint → style}/policy/vscode_extensions/target.json +0 -0
- /package/rules/{style-lint → style}/policy/vscode_extensions/template/extensions.json.snippet.json +0 -0
- /package/rules/{style-lint → style}/policy/vscode_settings/target.json +0 -0
- /package/rules/{style-lint → style}/policy/vscode_settings/template/settings.json.snippet.json +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.8.3] - 2026-06-22
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- ♻️ refactor(npm): Перехід з `style-lint` на `style` у правил та конфігах
|
|
8
|
+
|
|
9
|
+
## [12.8.2] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- ♻️ refactor(docs): Оновлення логіки та формату в документації
|
|
14
|
+
|
|
3
15
|
## [12.8.1] - 2026-06-22
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `npx \@nitra/cursor rename-yaml-extensions` — k8s `*.yml` → `*.yaml`, `.github` `*.yaml` → `*.yml` (опції: `--dry-run`, `--root=…`; див. bin/rename-yaml-extensions.mjs)
|
|
10
10
|
* `npx \@nitra/cursor post-tool-use-check` — точка входу PostToolUse hook Claude Code: читає stdin JSON,
|
|
11
11
|
* дістає `tool_input.file_path`, маршрутизує його у відповідні правила
|
|
12
|
-
* (`*.mjs` → `js-lint`, `*.vue` → `js-lint style
|
|
12
|
+
* (`*.mjs` → `js-lint`, `*.vue` → `js-lint style vue` тощо) і викликає
|
|
13
13
|
* `fix` лише з ними. Прописується автоматично в `.claude/settings.json`.
|
|
14
14
|
* `npx \@nitra/cursor lint` — data-driven оркестратор lint+конформності по `rules/<id>/meta.json` (`lint: per-file|full`):
|
|
15
15
|
* за замовчуванням fix-by-default по дельті vs origin (лише `per-file` правила); `--full` =
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ version: '1.0'
|
|
|
29
29
|
Кожен пункт — за схемою:
|
|
30
30
|
|
|
31
31
|
- **target** — `rule` | `skill` | `check`
|
|
32
|
-
- **id** — який саме (`lint`, `text`, `js
|
|
32
|
+
- **id** — який саме (`lint`, `text`, `js`, …)
|
|
33
33
|
- **kind** — `ambiguous-doc` | `missing-check` | `false-positive` | `no-autofix` | `recurring-pattern`
|
|
34
34
|
- **evidence** — конкретний `файл:рядок` або вивід команди з цього запуску
|
|
35
35
|
- **suggestion** — запропонована зміна
|
|
@@ -42,8 +42,8 @@ version: '1.0'
|
|
|
42
42
|
- skill `n-lint`, `ambiguous-doc` — крок 2 не каже, як діяти при частковому autofix
|
|
43
43
|
evidence: `oxlint --fix` лишив 3 no-autofix-помилки
|
|
44
44
|
suggestion: додати в SKILL.md підпункт про частковий autofix
|
|
45
|
-
- rule `js
|
|
46
|
-
evidence: jscpd впав, але `check js
|
|
45
|
+
- rule `js`, `missing-check` — немає програмної перевірки jscpd-порогу
|
|
46
|
+
evidence: jscpd впав, але `check js` цього не ловить
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
## Ефемерність — обовʼязково
|
package/rules/ga/ga.mdc
CHANGED
|
@@ -138,4 +138,4 @@ CLI робить preflight на `shellcheck` і `uv` (`uvx`) у `PATH`, поті
|
|
|
138
138
|
|
|
139
139
|
**MegaLinter:** не використовувати; прибрати workflow, конфіги (`.mega-linter.yml`, `.megalinter.yaml`, `.mega-linter.yaml`), залежності та згадки в CI / pre-commit / документації.
|
|
140
140
|
|
|
141
|
-
**`depcheck`:** не використовувати у `.github/workflows/*.yml` — мігровано на `knip` (див. `js
|
|
141
|
+
**`depcheck`:** не використовувати у `.github/workflows/*.yml` — мігровано на `knip` (див. `js.mdc`). Перевірка невикористаних залежностей виконується разом з рештою лінтерів у `lint-js`, окремий крок `npx depcheck` у workflow не потрібен і блокується полісі `ga.workflow_common`.
|
|
@@ -34,7 +34,7 @@ forbidden_step_substrings := {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
# Заборонені бінарки у `run:` кроках (ga.mdc). `depcheck` мігровано на `knip`
|
|
37
|
-
# у `
|
|
37
|
+
# у `js.mdc` — окремий крок у workflow не потрібен. Регексп ловить виклики
|
|
38
38
|
# через `npx`, `bunx`, `npm exec`, або як standalone-команду на початку рядка.
|
|
39
39
|
forbidden_run_command_patterns := {"depcheck": `(?:^|[\s;&|])(?:npx|bunx|npm exec|pnpm exec)?[ \t]*depcheck\b`}
|
|
40
40
|
|
|
@@ -62,7 +62,7 @@ min_uses_version_template := concat(" ", [
|
|
|
62
62
|
|
|
63
63
|
forbidden_run_command_template := concat(" ", [
|
|
64
64
|
"jobs.%s.steps[%d]: `%s` заборонено у workflow —",
|
|
65
|
-
"мігровано на knip (js
|
|
65
|
+
"мігровано на knip (js.mdc, ga.mdc)",
|
|
66
66
|
])
|
|
67
67
|
|
|
68
68
|
# ── Аліаси на input ────────────────────────────────────────────────────────
|
|
@@ -90,7 +90,7 @@ deny contains msg if {
|
|
|
90
90
|
|
|
91
91
|
# ── deny: depcheck у будь-якому `run:` ────────────────────────────────────
|
|
92
92
|
#
|
|
93
|
-
# `depcheck` мігровано на `knip` (js
|
|
93
|
+
# `depcheck` мігровано на `knip` (js.mdc); `knip` вже запускається у lint-js
|
|
94
94
|
# CI як частина `bunx knip` у скрипті, тож окремий depcheck-крок зайвий і має
|
|
95
95
|
# бути видалений з workflow-файлів.
|
|
96
96
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JS-провайдер для `n-cursor coverage`: збирає метрики покриття (`vitest run --coverage`)
|
|
3
3
|
* і мутаційного тестування (Stryker з vitest-runner + perTest) для JS/TS коду.
|
|
4
|
-
* Активується через `js
|
|
4
|
+
* Активується через `js` правило в `.n-cursor.json#rules`; реальна applies-логіка
|
|
5
5
|
* — у `detect(cwd)`.
|
|
6
6
|
*
|
|
7
7
|
* Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
|
|
@@ -42,7 +42,7 @@ export function scopeToRoot(changedFiles, cwd, jsRoot) {
|
|
|
42
42
|
return out
|
|
43
43
|
}
|
|
44
44
|
const VITEST_HINT =
|
|
45
|
-
'js
|
|
45
|
+
'js coverage: vitest відсутній у package.json — додай `vitest`, `@vitest/coverage-v8` та `@stryker-mutator/vitest-runner` у devDependencies (див. test.mdc)'
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Чи у пакеті встановлено vitest (через dependencies або devDependencies).
|
|
@@ -334,7 +334,7 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
|
|
|
334
334
|
const mutateSrc = scope ? scope.files.filter(f => !TEST_FILE.test(f)) : null
|
|
335
335
|
|
|
336
336
|
// 1. Coverage через vitest run --passWithNoTests --coverage (+ --changed у changed-режимі)
|
|
337
|
-
const lcovDir = await mkdtemp(join(tmpdir(), 'js-
|
|
337
|
+
const lcovDir = await mkdtemp(join(tmpdir(), 'js-cov-'))
|
|
338
338
|
let coverage
|
|
339
339
|
try {
|
|
340
340
|
const code = await runner.runJsCoverage(scope ? { cwd: jsRoot, lcovDir, base: scope.base } : { cwd: jsRoot, lcovDir })
|
|
@@ -364,7 +364,7 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
|
|
|
364
364
|
const mutationPath = join(jsRoot, 'reports', 'stryker', 'mutation.json')
|
|
365
365
|
if (!existsSync(mutationPath)) {
|
|
366
366
|
throw new Error(
|
|
367
|
-
'js
|
|
367
|
+
'js coverage: stryker не залишив mutation.json — ' +
|
|
368
368
|
'запусти `npx @nitra/cursor fix test` для встановлення canonical stryker.config.mjs, ' +
|
|
369
369
|
'або налаштуй його вручну'
|
|
370
370
|
)
|
|
@@ -411,7 +411,7 @@ export async function collect(cwd, opts = {}) {
|
|
|
411
411
|
const runner = opts.runner ?? defaultRunner
|
|
412
412
|
const changed = Array.isArray(opts.changedFiles)
|
|
413
413
|
const jsRoots = await resolveAllJsRoots(cwd)
|
|
414
|
-
if (jsRoots.length === 0) throw new Error('js
|
|
414
|
+
if (jsRoots.length === 0) throw new Error('js coverage: package.json не знайдено')
|
|
415
415
|
|
|
416
416
|
const results = []
|
|
417
417
|
for (const jsRoot of jsRoots) {
|
|
@@ -429,9 +429,9 @@ export async function collect(cwd, opts = {}) {
|
|
|
429
429
|
// Changed-режим: нема змінених JS у жодному root → тихо порожньо (це pass, не помилка).
|
|
430
430
|
if (changed) return []
|
|
431
431
|
console.error(
|
|
432
|
-
'js
|
|
432
|
+
'js coverage: жоден workspace не має тестів ' +
|
|
433
433
|
'(`*.test.{js,mjs}` у `tests/` або поряд із джерелом) — ' +
|
|
434
|
-
'додай тести або вилучи `js
|
|
434
|
+
'додай тести або вилучи `js` з .n-cursor.json#rules'
|
|
435
435
|
)
|
|
436
436
|
return []
|
|
437
437
|
}
|
package/rules/js/docs/fix.md
CHANGED
package/rules/js/docs/index.md
CHANGED
package/rules/js/docs/main.md
CHANGED
package/rules/js/js/check.mjs
CHANGED
|
@@ -24,7 +24,7 @@ async function checkEslintConfig(passFn, failFn, cwd) {
|
|
|
24
24
|
eslintPath = 'eslint.config.mjs'
|
|
25
25
|
passFn('eslint.config.mjs існує')
|
|
26
26
|
} else {
|
|
27
|
-
failFn('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js
|
|
27
|
+
failFn('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js.mdc)')
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
30
|
const eslintRaw = await readFile(join(cwd, eslintPath), 'utf8')
|
|
@@ -32,7 +32,7 @@ async function checkEslintConfig(passFn, failFn, cwd) {
|
|
|
32
32
|
{
|
|
33
33
|
needle: 'getConfig',
|
|
34
34
|
ok: `${eslintPath}: містить getConfig`,
|
|
35
|
-
err: `${eslintPath}: потрібен виклик getConfig (js
|
|
35
|
+
err: `${eslintPath}: потрібен виклик getConfig (js.mdc)`
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
needle: '@nitra/eslint-config',
|
|
@@ -42,7 +42,7 @@ async function checkEslintConfig(passFn, failFn, cwd) {
|
|
|
42
42
|
{
|
|
43
43
|
needle: '**/auto-imports.d.ts',
|
|
44
44
|
ok: `${eslintPath}: ignores містить **/auto-imports.d.ts`,
|
|
45
|
-
err: `${eslintPath}: додай у ignores запис **/auto-imports.d.ts (js
|
|
45
|
+
err: `${eslintPath}: додай у ignores запис **/auto-imports.d.ts (js.mdc)`
|
|
46
46
|
}
|
|
47
47
|
]
|
|
48
48
|
for (const { needle, ok, err } of checks) {
|
|
@@ -71,7 +71,7 @@ function checkPackageJsonTypeModule(label, pkg, passFn, failFn) {
|
|
|
71
71
|
if (pkg.type === 'module') {
|
|
72
72
|
passFn(`${label}: "type": "module"`)
|
|
73
73
|
} else {
|
|
74
|
-
failFn(`${label}: має містити "type": "module" (js
|
|
74
|
+
failFn(`${label}: має містити "type": "module" (js.mdc)`)
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -163,7 +163,7 @@ async function checkPackageJsonJsLint(passFn, failFn, cwd) {
|
|
|
163
163
|
async function checkOxlintRc(passFn, failFn, cwd) {
|
|
164
164
|
const oxPath = join(cwd, '.oxlintrc.json')
|
|
165
165
|
if (!existsSync(oxPath)) {
|
|
166
|
-
failFn('.oxlintrc.json не існує — додай конфіг oxlint (js
|
|
166
|
+
failFn('.oxlintrc.json не існує — додай конфіг oxlint (js.mdc)')
|
|
167
167
|
return
|
|
168
168
|
}
|
|
169
169
|
let oxCfg
|
|
@@ -204,14 +204,14 @@ async function checkLintJsWorkflows(passFn, failFn, cwd) {
|
|
|
204
204
|
if (existsSync(join(cwd, '.github/workflows/lint-js.yml'))) {
|
|
205
205
|
passFn('.github/workflows/lint-js.yml є (структуру перевіряє npx @nitra/cursor fix → js_lint.lint_js_yml)')
|
|
206
206
|
} else {
|
|
207
|
-
failFn('.github/workflows/lint-js.yml не існує — створи його (js
|
|
207
|
+
failFn('.github/workflows/lint-js.yml не існує — створи його (js.mdc)')
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
const lintYmlPath = join(cwd, '.github/workflows/lint.yml')
|
|
211
211
|
if (existsSync(lintYmlPath)) {
|
|
212
212
|
const lintYml = await readFile(lintYmlPath, 'utf8')
|
|
213
213
|
if (lintYml.includes('bunx oxlint') && lintYml.includes('bunx eslint') && lintYml.includes('jscpd')) {
|
|
214
|
-
failFn('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js
|
|
214
|
+
failFn('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js.mdc)')
|
|
215
215
|
} else {
|
|
216
216
|
passFn('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
|
|
217
217
|
}
|
|
@@ -223,7 +223,7 @@ async function checkLintJsWorkflows(passFn, failFn, cwd) {
|
|
|
223
223
|
* копіює канонічний `knip-canonical.json` з пакета `@nitra/cursor` як стартовий
|
|
224
224
|
* baseline; зміст подальших модифікацій локально не валідується (`entry` /
|
|
225
225
|
* `project` / `ignore` / `ignoreDependencies` / `ignoreBinaries` дозволені
|
|
226
|
-
* будь-які; це side effect — описано у js
|
|
226
|
+
* будь-які; це side effect — описано у js.mdc).
|
|
227
227
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
228
228
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
229
229
|
* @param {string} cwd корінь репозиторію
|
|
@@ -242,11 +242,11 @@ async function checkKnipConfig(passFn, failFn, cwd) {
|
|
|
242
242
|
return
|
|
243
243
|
}
|
|
244
244
|
await copyFile(KNIP_CANONICAL_JSON_PATH, knipPath)
|
|
245
|
-
passFn('knip.json створено з канонічного npm/rules/js
|
|
245
|
+
passFn('knip.json створено з канонічного npm/rules/js/js/data/tooling/knip-canonical.json (js.mdc)')
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
* Перевіряє відповідність проєкту правилам js
|
|
249
|
+
* Перевіряє відповідність проєкту правилам js.mdc
|
|
250
250
|
* @param {string} [cwd] корінь репозиторію
|
|
251
251
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
252
252
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: JS Module
|
|
3
3
|
title: check.mjs
|
|
4
|
-
resource: npm/rules/js
|
|
4
|
+
resource: npm/rules/js/js/check.mjs
|
|
5
5
|
docgen:
|
|
6
6
|
crc: f61768f2
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
@@ -10,7 +10,7 @@ docgen:
|
|
|
10
10
|
|
|
11
11
|
## Огляд
|
|
12
12
|
|
|
13
|
-
Модуль виконує перевірку конфігураційних файлів проєкту, забезпечуючи їхню відповідність встановленим стандартам. Він читає та аналізує конфігураційні файли, включаючи `package.json`, `.oxlintrc.json`, `knip.json`, `knip-canonical.json` та `.eslintrc.json`. Перевірка ґрунтується на логіці, визначеній у (js
|
|
13
|
+
Модуль виконує перевірку конфігураційних файлів проєкту, забезпечуючи їхню відповідність встановленим стандартам. Він читає та аналізує конфігураційні файли, включаючи `package.json`, `.oxlintrc.json`, `knip.json`, `knip-canonical.json` та `.eslintrc.json`. Перевірка ґрунтується на логіці, визначеній у (js.mdc) та (text.mdc). Функціонал реалізований у режимі лише читання, що означає відсутність змін у файловій системі чи базах даних. При виявленні проблем, система перехоплює помилки, працюючи у режимі fail-safe. Свідомо ігноруються шляхи `.github` та `.git`.
|
|
14
14
|
|
|
15
15
|
## Поведінка
|
|
16
16
|
|
|
@@ -30,7 +30,7 @@ docgen:
|
|
|
30
30
|
|
|
31
31
|
## Публічний API
|
|
32
32
|
|
|
33
|
-
check — забезпечує відповідність проєкту вимогам, описаним у js
|
|
33
|
+
check — забезпечує відповідність проєкту вимогам, описаним у js.mdc.
|
|
34
34
|
|
|
35
35
|
## Гарантії поведінки
|
|
36
36
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Directory Index
|
|
3
|
-
title: npm/rules/js
|
|
4
|
-
resource: npm/rules/js
|
|
3
|
+
title: npm/rules/js/js
|
|
4
|
+
resource: npm/rules/js/js/
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# npm/rules/js
|
|
7
|
+
# npm/rules/js/js
|
|
8
8
|
|
|
9
9
|
| Файл | Тип |
|
|
10
10
|
| ------------------------------------- | --------- |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: JS Module
|
|
3
3
|
title: utils_imports.mjs
|
|
4
|
-
resource: npm/rules/js
|
|
4
|
+
resource: npm/rules/js/js/utils_imports.mjs
|
|
5
5
|
docgen:
|
|
6
6
|
crc: 7eaeaf96
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Модуль `npm/rules/js
|
|
9
|
+
Модуль `npm/rules/js/js/utils_imports.mjs` реалізує одну з перевірок правила `js.mdc`: жоден файл усередині будь-якого каталогу з ім'ям `utils/` не має імпортувати щось за межами цього самого каталогу через відносні шляхи з префіксом `..`.
|
|
10
10
|
|
|
11
11
|
Філософія перевірки:
|
|
12
12
|
|
|
@@ -150,7 +150,7 @@ docgen:
|
|
|
150
150
|
|
|
151
151
|
### Інтеграція в перевірочний рантайм
|
|
152
152
|
|
|
153
|
-
Модуль викликається check-runner-ом правила `js
|
|
153
|
+
Модуль викликається check-runner-ом правила `js.mdc` (зазвичай із `npm/rules/js/js/`). Runner імпортує named export `check` і чекає на її resolved-значення як на process exit-code. Сам файл **не** має top-level executable коду — лише визначення; це дозволяє безпечно імпортувати його у тестах.
|
|
154
154
|
|
|
155
155
|
Типовий виклик (псевдокод):
|
|
156
156
|
|
|
@@ -167,7 +167,7 @@ process.exit(exitCode)
|
|
|
167
167
|
2. `check()` знаходить `utils/` каталоги в усіх package-roots.
|
|
168
168
|
3. Для кожного non-test source файлу витягає імпорти.
|
|
169
169
|
4. Жоден імпорт не починається з `..`.
|
|
170
|
-
5. `reporter.pass('utils-каталогів: N, перевірено M файлів — domain-bound імпортів немає (js
|
|
170
|
+
5. `reporter.pass('utils-каталогів: N, перевірено M файлів — domain-bound імпортів немає (js.mdc)')`.
|
|
171
171
|
6. `getExitCode() → 0`.
|
|
172
172
|
|
|
173
173
|
### Сценарій "є порушення"
|
|
@@ -175,7 +175,7 @@ process.exit(exitCode)
|
|
|
175
175
|
1. Знайдено файл `packages/foo/utils/helper.mjs`.
|
|
176
176
|
2. У ньому є `import bar from '../lib/bar.mjs'`.
|
|
177
177
|
3. `PARENT_RELATIVE_RE` матчить `../lib/bar.mjs`.
|
|
178
|
-
4. `reporter.fail('packages/foo/utils/helper.mjs: заборонений імпорт \'../lib/bar.mjs\' — utils/-файли мають бути generic (js
|
|
178
|
+
4. `reporter.fail('packages/foo/utils/helper.mjs: заборонений імпорт \'../lib/bar.mjs\' — utils/-файли мають бути generic (js.mdc)')`.
|
|
179
179
|
5. `violations` інкрементується.
|
|
180
180
|
6. По завершенню `getExitCode() → 1`.
|
|
181
181
|
|
|
@@ -183,7 +183,7 @@ process.exit(exitCode)
|
|
|
183
183
|
|
|
184
184
|
1. У жодному package немає каталогу `utils/`.
|
|
185
185
|
2. `utilsDirSet.size === 0`.
|
|
186
|
-
3. `reporter.pass('utils-каталогів немає — перевірку пропущено (js
|
|
186
|
+
3. `reporter.pass('utils-каталогів немає — перевірку пропущено (js.mdc)')`.
|
|
187
187
|
4. `getExitCode() → 0`.
|
|
188
188
|
|
|
189
189
|
### Сценарій "файл із синтаксичною помилкою"
|
|
@@ -207,6 +207,6 @@ process.exit(exitCode)
|
|
|
207
207
|
- **De-duplication** через `Set`: якщо monorepo-структура повертає однаковий `utils/`-шлях двічі (наприклад, `.`-root і назва вкладеного пакета перетинаються), він обробиться лише раз.
|
|
208
208
|
- **Помилки `readdir`** глушаться — недоступний каталог просто пропускається без падіння всієї перевірки.
|
|
209
209
|
|
|
210
|
-
### Залежність від конвенцій правила `js
|
|
210
|
+
### Залежність від конвенцій правила `js.mdc`
|
|
211
211
|
|
|
212
|
-
Файл є технічною реалізацією одного з пунктів правила `js
|
|
212
|
+
Файл є технічною реалізацією одного з пунктів правила `js.mdc`. Усі повідомлення `reporter.pass/fail` посилаються на `(js.mdc)`, щоб користувач знав, де читати про сам принцип розділу `utils/` ↔ `lib/`.
|
package/rules/js/js/tooling.mjs
CHANGED
|
@@ -141,7 +141,7 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
|
|
|
141
141
|
|
|
142
142
|
if (!deepEqualOxlintCanonical(actual, expected)) {
|
|
143
143
|
failures.push(
|
|
144
|
-
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js
|
|
144
|
+
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js/js/data/tooling/oxlint-canonical.json)`
|
|
145
145
|
)
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -153,7 +153,7 @@ export async function check() {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
if (utilsDirSet.size === 0) {
|
|
156
|
-
reporter.pass('utils-каталогів немає — перевірку пропущено (js
|
|
156
|
+
reporter.pass('utils-каталогів немає — перевірку пропущено (js.mdc)')
|
|
157
157
|
return reporter.getExitCode()
|
|
158
158
|
}
|
|
159
159
|
let violations = 0
|
|
@@ -167,7 +167,7 @@ export async function check() {
|
|
|
167
167
|
for (const src of imports) {
|
|
168
168
|
if (PARENT_RELATIVE_RE.test(src)) {
|
|
169
169
|
const rel = relative(root, file)
|
|
170
|
-
reporter.fail(`${rel}: заборонений імпорт '${src}' — utils/-файли мають бути generic (js
|
|
170
|
+
reporter.fail(`${rel}: заборонений імпорт '${src}' — utils/-файли мають бути generic (js.mdc)`)
|
|
171
171
|
violations += 1
|
|
172
172
|
}
|
|
173
173
|
}
|
|
@@ -175,7 +175,7 @@ export async function check() {
|
|
|
175
175
|
}
|
|
176
176
|
if (violations === 0) {
|
|
177
177
|
reporter.pass(
|
|
178
|
-
`utils-каталогів: ${utilsDirSet.size}, перевірено ${checkedFiles} файлів — domain-bound імпортів немає (js
|
|
178
|
+
`utils-каталогів: ${utilsDirSet.size}, перевірено ${checkedFiles} файлів — domain-bound імпортів немає (js.mdc)`
|
|
179
179
|
)
|
|
180
180
|
}
|
|
181
181
|
return reporter.getExitCode()
|
package/rules/js/js.mdc
CHANGED
|
@@ -5,7 +5,7 @@ alwaysApply: false
|
|
|
5
5
|
version: '1.30'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
**oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js
|
|
8
|
+
**oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у 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 окремо не додавати.
|
|
9
9
|
|
|
10
10
|
У кожному **`package.json`** проєкту (корінь і всі workspace-пакети) має бути **`"type": "module"`** — весь код у ESM.
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ version: '1.30'
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
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
|
|
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`** (CI — `--read-only`).
|
|
22
22
|
|
|
23
23
|
## Розширення нових файлів — `.mjs` / `.cjs`, не `.js`
|
|
24
24
|
|
|
@@ -35,7 +35,7 @@ version: '1.30'
|
|
|
35
35
|
|
|
36
36
|
У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
37
37
|
|
|
38
|
-
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js
|
|
38
|
+
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js/js/data/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Канон **`oxlint-canonical.json`** — source-of-truth, редагується напряму; у споживачі оновлюється копіюванням файлу з репозиторію пакета. Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
|
|
39
39
|
|
|
40
40
|
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
41
41
|
|
|
@@ -70,9 +70,33 @@ version: '1.30'
|
|
|
70
70
|
.claude/worktrees/
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## Залежнісна політика (що не додавати)
|
|
74
|
+
|
|
75
|
+
`@e18e/eslint-plugin` окремо не додавай — він уже в залежностях `@nitra/eslint-config` (з **3.8.0**), oxlint підвантажує його з `node_modules`. Пакети oxlint/eslint/jscpd/knip теж не додавай у `devDependencies` без потреби монорепо — `bunx` тягне їх ad-hoc.
|
|
76
|
+
|
|
73
77
|
## knip
|
|
74
78
|
|
|
75
|
-
Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон)
|
|
79
|
+
Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон) крос-файловий; запускається автоматично у `lint --full` разом з jscpd.
|
|
80
|
+
|
|
81
|
+
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js/js/tooling/knip-canonical.json`](./js/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`, `graphql`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
|
|
82
|
+
|
|
83
|
+
Якщо `knip.json` відсутній — `npx @nitra/cursor fix js` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
84
|
+
|
|
85
|
+
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
86
|
+
|
|
87
|
+
## Заборона `@nitra/as-integrations-fastify`
|
|
88
|
+
|
|
89
|
+
Пакет **`@nitra/as-integrations-fastify`** заборонений у **`dependencies`**, **`peerDependencies`** та в import-specifier-ах. Це чистий републіш upstream, застряглий на peer `@apollo/server: "^4.0.0"`, тож на Apollo 5 `bun install` дає `warn: incorrect peer dependency`. Заміна — upstream **`@as-integrations/fastify`** (**`^3.1.0`**): peer `@apollo/server: "^4.0.0 || ^5.0.0"` + `fastify: "^5.3.0"`.
|
|
90
|
+
|
|
91
|
+
Міграція — лише specifier у трьох місцях: залежність у `package.json`, `import`, та `vi.mock(...)` / `await import(...)` у тестах. **Код не міняється**, експорти ті самі: `default` → `fastifyApollo`, named → `fastifyApolloDrainPlugin`.
|
|
92
|
+
|
|
93
|
+
```javascript title="❌ до"
|
|
94
|
+
import fastifyApollo, { fastifyApolloDrainPlugin } from '@nitra/as-integrations-fastify'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```javascript title="✅ після"
|
|
98
|
+
import fastifyApollo, { fastifyApolloDrainPlugin } from '@as-integrations/fastify'
|
|
99
|
+
```
|
|
76
100
|
|
|
77
101
|
## jscpd: рефакторинг і структура
|
|
78
102
|
|
|
@@ -95,7 +119,7 @@ version: '1.30'
|
|
|
95
119
|
|
|
96
120
|
Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
|
|
97
121
|
|
|
98
|
-
Автоматично гарантується: `npx @nitra/cursor fix js
|
|
122
|
+
Автоматично гарантується: `npx @nitra/cursor fix js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail. Якщо потрібна domain-залежність — перенеси файл у `lib/`.
|
|
99
123
|
|
|
100
124
|
Додай workflow:
|
|
101
125
|
|
|
@@ -209,4 +233,4 @@ for (const item of arr) {
|
|
|
209
233
|
|
|
210
234
|
## Покриття + мутаційне тестування JS
|
|
211
235
|
|
|
212
|
-
Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js
|
|
236
|
+
Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js/coverage/coverage.mjs`: `bun test --coverage --coverage-reporter=lcov` + `bunx stryker run`. Stryker конфігурується в `stryker.config.mjs` у JS-корені (single-package або `workspaces[0]`).
|
package/rules/js/main.mjs
CHANGED
|
@@ -77,6 +77,20 @@ function lintFullProject(cwd, readOnly) {
|
|
|
77
77
|
return runInherit(readOnly ? ['eslint', '.'] : ['eslint', '--fix', '.'], cwd)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Крос-файловий аналіз: jscpd (дублікати) + knip (невикористані залежності/експорти).
|
|
82
|
+
* Ігнорує `files` — завжди по всьому репо.
|
|
83
|
+
* @param {string} cwd корінь репо
|
|
84
|
+
* @returns {number} exit code
|
|
85
|
+
*/
|
|
86
|
+
function lintFullCi(cwd) {
|
|
87
|
+
const jscpd = spawnSync('bunx', ['jscpd', '.'], { cwd, stdio: 'inherit' })
|
|
88
|
+
const jc = typeof jscpd.status === 'number' ? jscpd.status : 1
|
|
89
|
+
if (jc !== 0) return jc
|
|
90
|
+
const knip = spawnSync('bunx', ['knip', '--no-config-hints'], { cwd, stdio: 'inherit' })
|
|
91
|
+
return typeof knip.status === 'number' ? knip.status : 1
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
/**
|
|
81
95
|
* Quick-режим: авто-фікс змінених файлів, тоді класифікація лишених findings
|
|
82
96
|
* на introduced / pre-existing (беклог #6/A). Блокування на будь-якому finding.
|
|
@@ -102,7 +116,7 @@ function lintChangedClassified(js, cwd, readOnly) {
|
|
|
102
116
|
// Краш інструмента (ненульовий exit + непарсабельний json) НЕ можна тихо пропустити
|
|
103
117
|
// як «чисто» — це регресія проти старого fail-fast. Фейлимо явно.
|
|
104
118
|
if ((ox === null && oxRes.status !== 0) || (es === null && esRes.status !== 0)) {
|
|
105
|
-
process.stderr.write('❌ js
|
|
119
|
+
process.stderr.write('❌ js: інструмент завершився з помилкою (не lint-порушення) — json не розпарсено\n')
|
|
106
120
|
return 1
|
|
107
121
|
}
|
|
108
122
|
|
|
@@ -110,13 +124,14 @@ function lintChangedClassified(js, cwd, readOnly) {
|
|
|
110
124
|
if (findings.length === 0) return 0
|
|
111
125
|
|
|
112
126
|
const classified = classifyFindings(findings, addedLinesByFile(js, cwd), cwd)
|
|
113
|
-
const header = `❌ js
|
|
127
|
+
const header = `❌ js: ${findings.length} порушень (introduced ${classified.introduced.length}, pre-existing ${classified.preExisting.length})`
|
|
114
128
|
process.stdout.write(`${header}\n${renderFindings(classified, cwd)}\n`)
|
|
115
129
|
return 1
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
/**
|
|
119
|
-
* Запускає oxlint+eslint
|
|
133
|
+
* Запускає oxlint+eslint (per-file або full) + jscpd+knip (лише full).
|
|
134
|
+
* За замовчуванням — з автофіксом; `opts.readOnly` — лише детект.
|
|
120
135
|
* @param {string[] | undefined} files per-file: лише ці файли; undefined: весь проєкт (--full)
|
|
121
136
|
* @param {string} [cwd] корінь репо
|
|
122
137
|
* @param {{ readOnly?: boolean }} [opts] readOnly → без `--fix` (нуль мутацій)
|
|
@@ -125,7 +140,9 @@ function lintChangedClassified(js, cwd, readOnly) {
|
|
|
125
140
|
export function lint(files, cwd = process.cwd(), opts = {}) {
|
|
126
141
|
const readOnly = opts.readOnly === true
|
|
127
142
|
if (files === undefined) {
|
|
128
|
-
|
|
143
|
+
const esCode = lintFullProject(cwd, readOnly)
|
|
144
|
+
if (esCode !== 0) return Promise.resolve(esCode)
|
|
145
|
+
return Promise.resolve(lintFullCi(cwd))
|
|
129
146
|
}
|
|
130
147
|
const js = filterJsFiles(files)
|
|
131
148
|
if (js.length === 0) return Promise.resolve(0)
|
|
@@ -133,6 +150,6 @@ export function lint(files, cwd = process.cwd(), opts = {}) {
|
|
|
133
150
|
}
|
|
134
151
|
|
|
135
152
|
if (isRunAsCli(import.meta.url)) {
|
|
136
|
-
// Standalone: bun rules/js
|
|
153
|
+
// Standalone: bun rules/js/main.mjs — повний еквівалент `npx @nitra/cursor check js`.
|
|
137
154
|
process.exitCode = await runRuleCli(import.meta.dirname)
|
|
138
155
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Перевірка `.jscpd.json` (js
|
|
1
|
+
# Перевірка `.jscpd.json` (js.mdc).
|
|
2
2
|
#
|
|
3
3
|
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
4
|
# Структура --data сформована з template/.jscpd.json.snippet.json.
|
|
@@ -17,7 +17,7 @@ deny contains msg if {
|
|
|
17
17
|
not is_array(expected_value)
|
|
18
18
|
actual := object.get(input, key, null)
|
|
19
19
|
actual != expected_value
|
|
20
|
-
msg := sprintf(".jscpd.json має містити \"%s\": %v (js
|
|
20
|
+
msg := sprintf(".jscpd.json має містити \"%s\": %v (js.mdc)", [key, expected_value])
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
# Array subset-of для reporters.
|
|
@@ -27,7 +27,7 @@ deny contains msg if {
|
|
|
27
27
|
actual_set := {v | some v in object.get(input, field, [])}
|
|
28
28
|
some required in expected_values
|
|
29
29
|
not required in actual_set
|
|
30
|
-
msg := sprintf(".jscpd.json має містити \"%s\": [\"%s\"] (js
|
|
30
|
+
msg := sprintf(".jscpd.json має містити \"%s\": [\"%s\"] (js.mdc)", [field, required])
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
# minLines: must be number and >= expected.
|
|
@@ -35,7 +35,7 @@ deny contains msg if {
|
|
|
35
35
|
expected := data.template.snippet.minLines
|
|
36
36
|
actual := object.get(input, "minLines", null)
|
|
37
37
|
not is_valid_min_lines(actual, expected)
|
|
38
|
-
msg := sprintf(".jscpd.json має містити \"minLines\" як число >= %d (js
|
|
38
|
+
msg := sprintf(".jscpd.json має містити \"minLines\" як число >= %d (js.mdc)", [expected])
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
is_valid_min_lines(actual, expected) if {
|