@nitra/cursor 1.11.0 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +20 -3
  2. package/bin/n-cursor.js +10 -3
  3. package/package.json +1 -4
  4. package/rules/abie/utils/k8s-tree.mjs +1 -1
  5. package/rules/adr/js/{check.mjs → hooks/check.mjs} +2 -2
  6. package/rules/bun/js/{check.mjs → layout/check.mjs} +1 -1
  7. package/rules/capacitor/js/{check.mjs → platforms/check.mjs} +1 -1
  8. package/rules/changelog/js/{check.mjs → consistency/check.mjs} +2 -2
  9. package/rules/docker/js/{check.mjs → lint/check.mjs} +5 -5
  10. package/rules/ga/js/lint.mjs +1 -1
  11. package/rules/ga/js/{check.mjs → workflows/check.mjs} +4 -4
  12. package/rules/graphql/js/{check.mjs → tooling/check.mjs} +5 -5
  13. package/rules/hasura/js/{check.mjs → internal_urls/check.mjs} +4 -4
  14. package/rules/image-avif/js/{check.mjs → avif_generation/check.mjs} +5 -5
  15. package/rules/image-compress/js/{check.mjs → package_setup/check.mjs} +1 -1
  16. package/rules/js-bun-db/js/{check.mjs → safety/check.mjs} +5 -5
  17. package/rules/js-bun-redis/js/{check.mjs → imports/check.mjs} +4 -4
  18. package/rules/js-lint/js/{check.mjs → tooling/check.mjs} +16 -2
  19. package/rules/js-mssql/js/{check.mjs → deps/check.mjs} +5 -5
  20. package/rules/js-run/js/{check.mjs → runtime/check.mjs} +10 -10
  21. package/rules/k8s/js/{check.mjs → manifests/check.mjs} +4 -4
  22. package/rules/nginx-default-tpl/js/{check.mjs → template/check.mjs} +5 -5
  23. package/rules/npm-module/js/{check.mjs → package_structure/check.mjs} +4 -4
  24. package/rules/php/js/{check.mjs → tooling/check.mjs} +1 -1
  25. package/rules/style-lint/js/{check.mjs → tooling/check.mjs} +1 -1
  26. package/rules/tauri/js/{check.mjs → tooling/check.mjs} +2 -2
  27. package/rules/text/js/{check.mjs → formatting/check.mjs} +2 -2
  28. package/rules/vue/js/{check.mjs → packages/check.mjs} +5 -5
  29. package/scripts/auto-skills.mjs +92 -51
  30. package/scripts/utils/discover-checkable-rules.mjs +5 -22
  31. package/scripts/utils/run-rule.mjs +1 -5
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.11.3] - 2026-05-15
8
+
9
+ ### Fixed
10
+
11
+ - **`npm/bin/n-cursor.js`** — `detectAutoSkills` тепер отримує **ефективний** список правил (опт-ін вручну з `.n-cursor.json:rules` ∪ auto-detected, мінус `disable-rules`), а не лише auto-detected. Без цього скіли із залежністю на правило, додане вручну (наприклад, `adr` без `auto.md`-умови), не активувалися — у репо з `"rules": ["adr", …]` скіл `adr-normalize` залишався відсутнім, попри `[adr]` у його `skills/adr-normalize/auto.md`. Тепер `adr-normalize`, `abie-clean`, `abie-kustomize`, `taze` авто-додаються коректно як при auto-detected, так і при manual-opt-in відповідних правил.
12
+
13
+ ## [1.11.2] - 2026-05-15
14
+
15
+ ### Fixed
16
+
17
+ - **`npm/scripts/auto-skills.mjs`** — джерело правди для автоактивації скілів тепер `skills/<skill>/auto.md`, а не hardcoded мапа в JS. Парсер розпізнає три формати: `завжди` (always-on), `[rule, rule, …]` (умова на правила), відсутній/нерозпізнаний файл (opt-in). Експортовані константи `AUTO_SKILL_ORDER` та `AUTO_SKILL_RULE_DEPENDENCIES` тепер похідні від сканування `npm/skills/` під час завантаження модуля (зберігаються для зворотної сумісності). Побічно виправлено пропуск `abie-clean` у hardcoded мапі попри `[abie]` у його `auto.md` — тепер скіл коректно автоактивується разом з правилом `abie`.
18
+
19
+ ## [1.11.1] - 2026-05-15
20
+
21
+ ### Fixed
22
+
23
+ - **`npm/bin/n-cursor.js`** — `runSync()` (entry для `npx @nitra/cursor` без аргументів) шукав
24
+ `<packageRoot>/mdc` після того, як phase 1-4 перейменував каталог у `rules/`. Виправлено: тепер
25
+ вказує на коректний шлях `<packageRoot>/rules` — більше не кидає «Не знайдено каталог правил пакету».
26
+
7
27
  ## [1.11.0] - 2026-05-15
8
28
 
9
29
  ### Added
@@ -37,9 +57,6 @@
37
57
  - **Walk-glob правила** (6): `js-mssql`, `js-bun-db`, `js-bun-redis`, `js-run` (package_json + configmap), `vue`, `image-avif` — `walkGlob: "**/package.json"` або відповідний патерн.
38
58
  - **k8s.* концерни** (8): `manifest`, `gateway`, `hpa_pdb`, `kustomization`, `svc_yaml`, `svc_hl_yaml`, `base_kustomization`, `base_manifest` — `walkGlob` по YAML під сегментом `k8s/`; `base_manifest` використовує негативний glob для виключення `kustomization.yaml`.
39
59
  - **abie концерни** (4): `clean_merged_ignore_branches` (single), `health_check_policy` (walkGlob `**/k8s/**/hc.yaml`), `http_route_base` (walkGlob `**/k8s/**/base/**/hr.yaml`), `base_deployment_preem` (walkGlob `**/k8s/**/base/**/*.{yaml,yml}` з виключенням `kustomization.yaml`).
40
-
41
- ### Changed
42
-
43
60
  - **`capture-decisions.sh` тепер пише чернетки напряму в `docs/adr/<timestamp>-<sid>.md`** (раніше — у `docs/adr/_inbox/`). Сам каталог `_inbox/` більше не створюється, але `normalize-decisions.sh` бачить його рекурсивно — старі чернетки з `_inbox/` поступово розчищаються нормалізацією. Можна також одноразово `git mv docs/adr/_inbox/*.md docs/adr/` і прибрати порожній каталог.
44
61
  - **Правило `adr` (`npm/rules/adr/adr.mdc`)**: повне переписування під дві фази (capture + normalize). Видалено згадки `_inbox/`. Версія `version: '2.0'`.
45
62
  - **`npm/rules/adr/js/check.mjs`**: перевірка обох hook-скриптів (canonicity), обох log-файлів у `.gitignore`.
package/bin/n-cursor.js CHANGED
@@ -261,9 +261,16 @@ async function readConfig(paths = {}) {
261
261
  packageJsonParsed: rootPkg,
262
262
  disableRules
263
263
  })
264
+ // Skills залежать від ефективного списку правил, який буде у конфізі після merge:
265
+ // вже існуючі (опт-ін вручну) + auto-detected, мінус `disable-rules`. Без цього
266
+ // правило, додане вручну (напр. `adr` без auto.md-умови), не активувало б залежні
267
+ // скіли (`adr-normalize`).
268
+ const disableRulesSet = new Set(disableRules)
269
+ const effectiveRulesForSkills = [...new Set([...normalizeIdList(parsedConfig.rules), ...autoDetectedRules.rules])]
270
+ .filter(id => !disableRulesSet.has(id))
264
271
  const autoDetectedSkills = detectAutoSkills({
265
272
  availableSkills,
266
- detectedRules: autoDetectedRules.rules,
273
+ detectedRules: effectiveRulesForSkills,
267
274
  disableSkills
268
275
  })
269
276
 
@@ -945,7 +952,7 @@ async function runSyncStep(prefix, action) {
945
952
  /**
946
953
  * Копіює керовані `.mdc` файли з пакету до `.cursor/rules`.
947
954
  * @param {string[]} rules список rules з конфігу
948
- * @param {string} bundledRulesDir каталог `mdc` пакету-джерела
955
+ * @param {string} bundledRulesDir каталог `rules` пакету-джерела
949
956
  * @param {string} rulesDir абсолютний шлях до `.cursor/rules`
950
957
  * @returns {Promise<{ successCount: number, failCount: number }>} статистика копіювання
951
958
  */
@@ -1207,7 +1214,7 @@ async function runSync() {
1207
1214
 
1208
1215
  await reexecIfPackageVersionChanged(effectivePackageRoot)
1209
1216
 
1210
- const bundledRulesDir = join(effectivePackageRoot, 'mdc')
1217
+ const bundledRulesDir = join(effectivePackageRoot, 'rules')
1211
1218
  const bundledSkillsDir = join(effectivePackageRoot, 'skills')
1212
1219
  const bundledAgentsTemplatePath = join(effectivePackageRoot, AGENTS_TEMPLATE_FILE)
1213
1220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.11.0",
3
+ "version": "1.11.3",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -51,8 +51,5 @@
51
51
  "engines": {
52
52
  "bun": ">=1.3",
53
53
  "node": ">=25"
54
- },
55
- "devDependencies": {
56
- "@nitra/cursor": "^1.9.22"
57
54
  }
58
55
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { dirname, relative } from 'node:path'
11
11
 
12
- import { pathHasK8sSegment } from '../../k8s/js/check.mjs'
12
+ import { pathHasK8sSegment } from '../../k8s/js/manifests/check.mjs'
13
13
  import { walkDir } from '../../../scripts/utils/walkDir.mjs'
14
14
  import { isDeploymentDoc, readAndParseYamlDocs } from './yaml.mjs'
15
15
 
@@ -22,7 +22,7 @@ import { delimiter, dirname, join } from 'node:path'
22
22
  import { env } from 'node:process'
23
23
  import { fileURLToPath } from 'node:url'
24
24
 
25
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
25
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
26
26
 
27
27
  /** Один hook-артефакт: bash-скрипт + його лог-файл, які перевіряємо однотипно. */
28
28
  const HOOK_ARTIFACTS = /** @type {const} */ ([
@@ -34,7 +34,7 @@ const PROJECT_SETTINGS_PATH = '.claude/settings.json'
34
34
  const EOL_RE = /\r?\n/u
35
35
 
36
36
  const here = dirname(fileURLToPath(import.meta.url))
37
- const BUNDLED_HOOKS_DIR = join(here, '..', '..', '..', '.claude-template', 'hooks')
37
+ const BUNDLED_HOOKS_DIR = join(here, '..', '..', '..', '..', '.claude-template', 'hooks')
38
38
 
39
39
  /**
40
40
  * Відносний шлях до managed hook-скрипта у проєкті.
@@ -18,7 +18,7 @@
18
18
  import { existsSync } from 'node:fs'
19
19
  import { readFile } from 'node:fs/promises'
20
20
 
21
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
21
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
22
22
 
23
23
  // Перевірка `devDependencies` кореневого `package.json` (дозволено лише `@nitra/*`)
24
24
  // — у rego (`npm/policy/bun/package_json/`). JS-копії `isAllowedRootDevDependency`
@@ -25,7 +25,7 @@ import { existsSync } from 'node:fs'
25
25
  import { readdir, readFile } from 'node:fs/promises'
26
26
  import { join, relative } from 'node:path'
27
27
 
28
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
28
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
29
29
 
30
30
  /** Мінімальна допустима мажорна версія Capacitor (capacitor.mdc) */
31
31
  const MIN_CAPACITOR_MAJOR = 8
@@ -27,8 +27,8 @@ import { readFile } from 'node:fs/promises'
27
27
  import { join } from 'node:path'
28
28
  import { promisify } from 'node:util'
29
29
 
30
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
31
- import { getMonorepoPackageRootDirs } from '../../../scripts/utils/workspaces.mjs'
30
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
31
+ import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
32
32
 
33
33
  const execFileAsync = promisify(execFile)
34
34
 
@@ -30,11 +30,11 @@
30
30
  import { readFile } from 'node:fs/promises'
31
31
  import { basename } from 'node:path'
32
32
 
33
- import { getMirrorGcrHint, getFromImageToken } from '../../../scripts/utils/docker-mirror.mjs'
34
- import { lintDockerfileWithHadolint, posixRel } from '../../../scripts/utils/docker-hadolint.mjs'
35
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
36
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
37
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
33
+ import { getMirrorGcrHint, getFromImageToken } from '../../../../scripts/utils/docker-mirror.mjs'
34
+ import { lintDockerfileWithHadolint, posixRel } from '../../../../scripts/utils/docker-hadolint.mjs'
35
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
36
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
37
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
38
38
 
39
39
  const NEWLINE_RE = /\r?\n/
40
40
  const BUN_INSTALL_RE = /\bbun\s+(?:install|i)\b/iu
@@ -22,7 +22,7 @@
22
22
  import { spawnSync } from 'node:child_process'
23
23
  import { platform } from 'node:process'
24
24
 
25
- import { check as checkGa } from './check.mjs'
25
+ import { check as checkGa } from './workflows/check.mjs'
26
26
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
27
27
 
28
28
  /**
@@ -19,10 +19,10 @@ import { readdir, readFile } from 'node:fs/promises'
19
19
  import { execFileSync } from 'node:child_process'
20
20
  import { join } from 'node:path'
21
21
 
22
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
23
- import { eventPathsIncludeExact, parseWorkflowYaml } from '../../../scripts/utils/gha-workflow.mjs'
24
- import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
25
- import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
22
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
23
+ import { eventPathsIncludeExact, parseWorkflowYaml } from '../../../../scripts/utils/gha-workflow.mjs'
24
+ import { resolveCmd } from '../../../../scripts/utils/resolve-cmd.mjs'
25
+ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
26
26
 
27
27
  /** Шаблони наявності MegaLinter у вмісті workflow */
28
28
  const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/megalinter/i]
@@ -10,15 +10,15 @@ import { existsSync } from 'node:fs'
10
10
  import { readFile } from 'node:fs/promises'
11
11
  import { relative } from 'node:path'
12
12
 
13
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
13
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
14
14
  import {
15
15
  isGqlScanSourceFile,
16
16
  shouldSkipFileForGqlScan,
17
17
  sourceFileHasGqlTaggedTemplate
18
- } from '../../../scripts/utils/graphql-gql-scan.mjs'
19
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
20
- import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
21
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
18
+ } from '../../../../scripts/utils/graphql-gql-scan.mjs'
19
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
20
+ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
21
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
22
22
 
23
23
  /** Очікуваний файл GraphQL Config у корені (graphql.mdc). */
24
24
  export const GRAPHQL_RC_FILENAME = '.graphqlrc.yml'
@@ -27,10 +27,10 @@ import { basename, join, relative } from 'node:path'
27
27
 
28
28
  import { parseAllDocuments } from 'yaml'
29
29
 
30
- import { getRepositoryUrl } from '../../../scripts/auto-rules.mjs'
31
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
32
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
33
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
30
+ import { getRepositoryUrl } from '../../../../scripts/auto-rules.mjs'
31
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
32
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
33
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
34
34
 
35
35
  const NITRA_REPOSITORY_URL_MARKER = 'https://github.com/nitra/'
36
36
  const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
@@ -25,11 +25,11 @@ import { join, relative } from 'node:path'
25
25
  import { spawnSync } from 'node:child_process'
26
26
  import { env } from 'node:process'
27
27
 
28
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
29
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
30
- import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
31
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
32
- import { getMonorepoPackageRootDirs } from '../../../scripts/utils/workspaces.mjs'
28
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
29
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
30
+ import { resolveCmd } from '../../../../scripts/utils/resolve-cmd.mjs'
31
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
32
+ import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
33
33
 
34
34
  /** Імʼя CLI-пакета, який генерує AVIF. */
35
35
  const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
@@ -19,7 +19,7 @@
19
19
  import { existsSync } from 'node:fs'
20
20
  import { readFile } from 'node:fs/promises'
21
21
 
22
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
22
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
23
23
 
24
24
  /** Імʼя committed-кешу (sha1 + originalSize + size) у `@nitra/minify-image` ≥ 3.2.0. */
25
25
  const HASH_CACHE_FILENAME = '.n-minify-image.tsv'
@@ -25,7 +25,7 @@ import { existsSync } from 'node:fs'
25
25
  import { readFile } from 'node:fs/promises'
26
26
  import { join, relative } from 'node:path'
27
27
 
28
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
28
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
29
29
  import {
30
30
  findBunSqlPerRequestConnectionInText,
31
31
  findBunSqlPgLeftoverCallInText,
@@ -36,10 +36,10 @@ import {
36
36
  findUnsafeBunSqlInListMissingEmptyGuardInText,
37
37
  isBunSqlScanSourceFile,
38
38
  textHasBunSqlImport
39
- } from '../../../scripts/utils/bun-sql-scan.mjs'
40
- import { findAllPackageJsonPaths } from '../../../scripts/utils/find-package-json-paths.mjs'
41
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
42
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
39
+ } from '../../../../scripts/utils/bun-sql-scan.mjs'
40
+ import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-json-paths.mjs'
41
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
42
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
43
43
 
44
44
  /**
45
45
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
@@ -14,10 +14,10 @@ import { existsSync } from 'node:fs'
14
14
  import { readFile } from 'node:fs/promises'
15
15
  import { join, relative } from 'node:path'
16
16
 
17
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
18
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
19
- import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from '../../../scripts/utils/redis-imports.mjs'
20
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
17
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
18
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
19
+ import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from '../../../../scripts/utils/redis-imports.mjs'
20
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
21
21
 
22
22
  /**
23
23
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану заборонених redis-імпортів.
@@ -16,17 +16,31 @@ import { copyFile, readFile } from 'node:fs/promises'
16
16
  import { dirname, join } from 'node:path'
17
17
  import { fileURLToPath } from 'node:url'
18
18
 
19
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
19
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
20
20
 
21
21
  /** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
22
22
  export const OXLINT_CANONICAL_JSON_PATH = join(
23
23
  dirname(fileURLToPath(import.meta.url)),
24
+ '..',
25
+ '..',
26
+ '..',
27
+ '..',
28
+ 'scripts',
24
29
  'utils',
25
30
  'oxlint-canonical.json'
26
31
  )
27
32
 
28
33
  /** Шлях до канонічного knip JSON у цьому пакеті — копіюється у корінь проєкту-споживача, якщо відсутній. */
29
- export const KNIP_CANONICAL_JSON_PATH = join(dirname(fileURLToPath(import.meta.url)), 'utils', 'knip-canonical.json')
34
+ export const KNIP_CANONICAL_JSON_PATH = join(
35
+ dirname(fileURLToPath(import.meta.url)),
36
+ '..',
37
+ '..',
38
+ '..',
39
+ '..',
40
+ 'scripts',
41
+ 'utils',
42
+ 'knip-canonical.json'
43
+ )
30
44
 
31
45
  /** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
32
46
  export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
@@ -12,8 +12,8 @@ import { existsSync } from 'node:fs'
12
12
  import { readFile } from 'node:fs/promises'
13
13
  import { join, relative } from 'node:path'
14
14
 
15
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
16
- import { findAllPackageJsonPaths } from '../../../scripts/utils/find-package-json-paths.mjs'
15
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
16
+ import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-json-paths.mjs'
17
17
  import {
18
18
  findMssqlPerRequestConnectionInText,
19
19
  findSharedMssqlRequestInText,
@@ -22,9 +22,9 @@ import {
22
22
  findUnsafeMssqlInListUnparsedInText,
23
23
  findUnsafeMssqlInListMissingEmptyGuardInText,
24
24
  isMssqlScanSourceFile
25
- } from '../../../scripts/utils/mssql-pool-scan.mjs'
26
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
27
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
25
+ } from '../../../../scripts/utils/mssql-pool-scan.mjs'
26
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
27
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
28
28
 
29
29
  const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/u
30
30
  const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/u
@@ -37,21 +37,21 @@ import {
37
37
  findBunyanImportsInText,
38
38
  isBunyanScanSourceFile,
39
39
  shouldSkipFileForBunyanScan
40
- } from '../../../scripts/utils/bunyan-imports.mjs'
41
- import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '../../../scripts/utils/check-env-scan.mjs'
42
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
43
- import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
44
- import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '../../../scripts/utils/conn-file-rules.mjs'
40
+ } from '../../../../scripts/utils/bunyan-imports.mjs'
41
+ import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '../../../../scripts/utils/check-env-scan.mjs'
42
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
43
+ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
44
+ import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '../../../../scripts/utils/conn-file-rules.mjs'
45
45
  import {
46
46
  findConnFactoryImportsInText,
47
47
  isConnImportsScanSourceFile,
48
48
  isInsideConnDir,
49
49
  resolveConnDirFromPackageJson
50
- } from '../../../scripts/utils/conn-imports-scan.mjs'
51
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
52
- import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '../../../scripts/utils/promise-settimeout-scan.mjs'
53
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
54
- import { getMonorepoPackageRootDirs } from '../../../scripts/utils/workspaces.mjs'
50
+ } from '../../../../scripts/utils/conn-imports-scan.mjs'
51
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
52
+ import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '../../../../scripts/utils/promise-settimeout-scan.mjs'
53
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
54
+ import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
55
55
 
56
56
  /**
57
57
  * Чи існує непорожній за змістом маркер каталогу `src/` (рекомендована структура js-run).
@@ -134,10 +134,10 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
134
134
 
135
135
  import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
136
136
 
137
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
138
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
139
- import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
140
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
137
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
138
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
139
+ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
140
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
141
141
 
142
142
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
143
143
  const YANNH_PIN = 'v1.33.9-standalone-strict'
@@ -17,11 +17,11 @@ import { existsSync } from 'node:fs'
17
17
  import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
18
18
  import { basename, dirname, join, relative } from 'node:path'
19
19
 
20
- import { findDockerfilePaths } from '../../docker/js/check.mjs'
21
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
22
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
23
- import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
24
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
20
+ import { findDockerfilePaths } from '../../../docker/js/lint/check.mjs'
21
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
22
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
23
+ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
24
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
25
25
 
26
26
  const LINE_SPLIT_RE = /\r?\n/u
27
27
  const INI_KEY_RE = /^([A-Za-z_]\w*)\s*=/u
@@ -32,10 +32,10 @@ import { promisify } from 'node:util'
32
32
 
33
33
  import { parseSync } from 'oxc-parser'
34
34
 
35
- import { dynamicImportModule, langFromPath, requireCallModule, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
36
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
37
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
38
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
35
+ import { dynamicImportModule, langFromPath, requireCallModule, walkAstWithAncestors } from '../../../../scripts/utils/ast-scan-utils.mjs'
36
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
37
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
38
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
39
39
 
40
40
  const execFileAsync = promisify(execFile)
41
41
 
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { existsSync } from 'node:fs'
13
13
 
14
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
14
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
15
15
 
16
16
  /**
17
17
  * Перевіряє відповідність проєкту правилам php.mdc.
@@ -19,7 +19,7 @@
19
19
  import { existsSync } from 'node:fs'
20
20
  import { readFile } from 'node:fs/promises'
21
21
 
22
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
22
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
23
23
 
24
24
  /**
25
25
  * Альтернатива полю `stylelint` у `package.json` — зовнішній файл конфігу. Якщо
@@ -18,8 +18,8 @@
18
18
  import { existsSync, statSync } from 'node:fs'
19
19
  import { readFile } from 'node:fs/promises'
20
20
 
21
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
22
- import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
21
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
22
+ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
23
23
 
24
24
  /**
25
25
  * Чи є префікс `@tauri-apps/` у ключах `dependencies` або `devDependencies`.
@@ -32,8 +32,8 @@
32
32
  import { existsSync } from 'node:fs'
33
33
  import { readFile } from 'node:fs/promises'
34
34
 
35
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
36
- import { anyRunStepIncludes, parseWorkflowYaml } from '../../../scripts/utils/gha-workflow.mjs'
35
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
36
+ import { anyRunStepIncludes, parseWorkflowYaml } from '../../../../scripts/utils/gha-workflow.mjs'
37
37
 
38
38
  /** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
39
39
  const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
@@ -21,16 +21,16 @@ import { existsSync } from 'node:fs'
21
21
  import { readFile } from 'node:fs/promises'
22
22
  import { join, relative } from 'node:path'
23
23
 
24
- import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
24
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
25
25
  import {
26
26
  findForbiddenNodeImportsInVueFile,
27
27
  findForbiddenVueImportsInSourceFile,
28
28
  isVueImportScanSourceFile,
29
29
  shouldSkipFileForVueImportScan
30
- } from '../../../scripts/utils/vue-forbidden-imports.mjs'
31
- import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
32
- import { walkDir } from '../../../scripts/utils/walkDir.mjs'
33
- import { getMonorepoPackageRootDirs } from '../../../scripts/utils/workspaces.mjs'
30
+ } from '../../../../scripts/utils/vue-forbidden-imports.mjs'
31
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
32
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
33
+ import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
34
34
 
35
35
  const ESBUILD_RE = /\besbuild\b/
36
36
 
@@ -1,81 +1,122 @@
1
1
  /**
2
- * Автовизначення skills для `.n-cursor.json` за умовами з `npm/skills/<skill>/auto.md`.
2
+ * Автовизначення skills для `.n-cursor.json` за умовами зі `npm/skills/<skill>/auto.md`.
3
3
  *
4
- * Скіли автододаються залежно від уже виявлених правил (auto-rules) щоб не дублювати
5
- * умови, які вже формалізовані для відповідного правила. Наприклад:
4
+ * `auto.md` джерело правди не hardcoded мапа). Підтримуються три варіанти:
6
5
  *
7
- * - `abie-kustomize - [abie]` додається разом з правилом `abie`
8
- * - `taze - [bun]` — додається разом з правилом `bun`
6
+ * - `завжди` скіл активується незалежно від інших правил
7
+ * (приклади: `fix`, `lint`, `llm-patch`, `publish-telegram`).
8
+ * - `[rule, rule, …]` — скіл активується, якщо ВСІ перелічені правила вже виявлені
9
+ * auto-rules (приклади: `abie-clean - [abie]`, `taze - [bun]`).
10
+ * - файл відсутній або формат не розпізнано — скіл opt-in лише через `.n-cursor.json:skills`.
9
11
  *
10
- * Скіли без секції `[rules]` у `skills/<skill>/auto.md` (`fix`, `lint`, `llm-patch`, `publish-telegram`)
11
- * додаються завжди, якщо доступні в пакеті й не у `disable-skills`.
12
+ * Сканування `npm/skills/` sync під час завантаження модуля (детермінізм + sync API
13
+ * `auto-rules.mjs`-сусіда). Кеш на час процесу.
12
14
  */
15
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
16
+ import { dirname, join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
13
18
 
14
- /** Порядок автододавання skills відповідно до `skills/<skill>/auto.md`. */
15
- export const AUTO_SKILL_ORDER = Object.freeze([
16
- 'abie-kustomize',
17
- 'adr-normalize',
18
- 'fix',
19
- 'lint',
20
- 'llm-patch',
21
- 'publish-telegram',
22
- 'taze'
23
- ])
19
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
20
+ const SKILLS_DIR = join(PACKAGE_ROOT, 'skills')
21
+
22
+ const ALWAYS_LITERAL = 'завжди'
23
+ const BRACKET_LIST_RE = /^\[([^\]]+)\]$/u
24
24
 
25
25
  /**
26
- * Залежність скілів від правил (`skills/<skill>/auto.md` синтаксис `skill - [rules]`).
27
- * Ключ варто автододати, коли всі правила-залежності вже додані до конфігу автодетектом.
26
+ * @typedef {{ always: true } | { rules: readonly string[] }} SkillAutoSpec
28
27
  */
29
- export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
30
- /** @type {Record<string, readonly string[]>} */ ({
31
- 'abie-kustomize': Object.freeze(['abie']),
32
- 'adr-normalize': Object.freeze(['adr']),
33
- taze: Object.freeze(['bun'])
34
- })
28
+
29
+ /**
30
+ * Парсить тіло `auto.md` одного скіла.
31
+ * @param {string} text вміст файла (без `trim`)
32
+ * @returns {SkillAutoSpec | null} `null` — формат не розпізнано (= opt-in)
33
+ */
34
+ function parseSkillAutoSpec(text) {
35
+ const trimmed = text.trim()
36
+ if (trimmed === ALWAYS_LITERAL) {
37
+ return { always: true }
38
+ }
39
+ const m = trimmed.match(BRACKET_LIST_RE)
40
+ if (m) {
41
+ const rules = m[1]
42
+ .split(',')
43
+ .map(s => s.trim())
44
+ .filter(s => s.length > 0)
45
+ if (rules.length === 0) return null
46
+ return { rules: Object.freeze(rules) }
47
+ }
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Сканує `npm/skills/<id>/auto.md`. Скіли без `auto.md` або з нерозпізнаним
53
+ * вмістом не потрапляють у результат — їх можна вмикати лише вручну в конфізі.
54
+ * @param {string} [skillsDir] override для тестів
55
+ * @returns {Record<string, SkillAutoSpec>}
56
+ */
57
+ export function discoverSkillAutoActivation(skillsDir = SKILLS_DIR) {
58
+ if (!existsSync(skillsDir)) return {}
59
+ /** @type {Record<string, SkillAutoSpec>} */
60
+ const out = {}
61
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
62
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue
63
+ const autoMdPath = join(skillsDir, entry.name, 'auto.md')
64
+ if (!existsSync(autoMdPath)) continue
65
+ const spec = parseSkillAutoSpec(readFileSync(autoMdPath, 'utf8'))
66
+ if (spec) out[entry.name] = spec
67
+ }
68
+ return out
69
+ }
70
+
71
+ /** Cache на час процесу: один скан `npm/skills/` дає всю автоактивацію. */
72
+ const SKILL_AUTO_ACTIVATION = discoverSkillAutoActivation()
73
+
74
+ /**
75
+ * Стабільний алфавітний порядок скілів з автоактивацією. Експортовано для зворотної
76
+ * сумісності (попередня версія мала жорстко прописаний `AUTO_SKILL_ORDER`).
77
+ */
78
+ export const AUTO_SKILL_ORDER = Object.freeze(
79
+ Object.keys(SKILL_AUTO_ACTIVATION).toSorted((a, b) => a.localeCompare(b))
35
80
  )
36
81
 
37
- /** Скіли без залежностей — додаються завжди (рядок «завжди» в `skills/<skill>/auto.md`). */
38
- const ALWAYS_ON_SKILLS = Object.freeze(['fix', 'lint', 'llm-patch', 'publish-telegram'])
82
+ /**
83
+ * Похідна view на `SKILL_AUTO_ACTIVATION`: лише скіли з rule-залежностями.
84
+ * Експортовано для зворотної сумісності та автодоку.
85
+ */
86
+ export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
87
+ Object.fromEntries(
88
+ Object.entries(SKILL_AUTO_ACTIVATION)
89
+ .filter(([, spec]) => 'rules' in spec)
90
+ .map(([id, spec]) => [id, /** @type {{ rules: readonly string[] }} */ (spec).rules])
91
+ )
92
+ )
39
93
 
40
94
  const DEFAULT_DISABLED_LIST = Object.freeze([])
41
95
 
42
96
  /**
43
- * Визначає авто-skills згідно з `skills/<skill>/auto.md`.
97
+ * Визначає авто-skills згідно з вмістом `skills/<skill>/auto.md`.
44
98
  * @param {object} params параметри
45
99
  * @param {string[]} params.availableSkills перелік доступних skills із пакету (id без префікса n-)
46
100
  * @param {string[]} params.detectedRules id правил, виявлених auto-rules (вхідні залежності)
47
101
  * @param {string[]} [params.disableSkills] список `disable-skills` з конфігу
48
- * @returns {{ skills: string[] }} список id у стабільному порядку (за `AUTO_SKILL_ORDER`)
102
+ * @returns {{ skills: string[] }} список id у стабільному алфавітному порядку
49
103
  */
50
104
  export function detectAutoSkills({ availableSkills, detectedRules, disableSkills = DEFAULT_DISABLED_LIST }) {
51
105
  const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
52
106
  const disableSkillsSet = new Set(disableSkills)
53
107
  const detectedRulesSet = new Set(detectedRules)
54
108
 
55
- /** @type {string[]} */
56
- const detected = []
57
-
58
- /**
59
- * Додає skill до результату, якщо він доступний і не в disable-списку.
60
- * @param {string} skillId id skill
61
- * @returns {void}
62
- */
63
- function addSkill(skillId) {
64
- if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detected.includes(skillId)) {
65
- return
66
- }
67
- detected.push(skillId)
68
- }
69
-
70
- for (const skillId of ALWAYS_ON_SKILLS) {
71
- addSkill(skillId)
72
- }
109
+ /** @type {Set<string>} */
110
+ const detected = new Set()
73
111
 
74
- for (const [skillId, deps] of Object.entries(AUTO_SKILL_RULE_DEPENDENCIES)) {
75
- if (deps.every(d => detectedRulesSet.has(d))) {
76
- addSkill(skillId)
112
+ for (const [skillId, spec] of Object.entries(SKILL_AUTO_ACTIVATION)) {
113
+ if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId)) continue
114
+ if ('always' in spec) {
115
+ detected.add(skillId)
116
+ } else if (spec.rules.every(d => detectedRulesSet.has(d))) {
117
+ detected.add(skillId)
77
118
  }
78
119
  }
79
120
 
80
- return { skills: AUTO_SKILL_ORDER.filter(id => detected.includes(id)) }
121
+ return { skills: AUTO_SKILL_ORDER.filter(id => detected.has(id)) }
81
122
  }
@@ -2,11 +2,11 @@
2
2
  * Discovery rules для CLI `check`. Шукає правила, для яких є щось «прогонне»:
3
3
  * - JS concerns: `rules/<id>/js/<concern>/<check.mjs | check-*.mjs>` — кожен concern окремий вузол.
4
4
  * - Policy concerns: `rules/<id>/policy/<concern>/target.json` — пара з `<concern>.rego`.
5
- * - Legacy JS (на час міграції): `rules/<id>/js/check.mjs` (плаский) — мапиться у concern `legacy`,
6
- * щоб не ламати ще не мігровані правила.
7
5
  *
8
6
  * Каталог `utils/` всередині `js/` свідомо пропускається — це хелпери, не концерни.
9
7
  * Файли `*.test.mjs` фільтруються regex (`^check(?:-.+)?\.mjs$`).
8
+ * Top-level плаский `js/check.mjs` (legacy) більше не підтримується — усі вшиті правила
9
+ * у пакеті розпиляні на concern-структуру.
10
10
  *
11
11
  * Намеренно НЕ парсимо `target.json` тут (це робить runner). Discovery — швидкий скан структури:
12
12
  * шляхи + назви, без I/O вмісту.
@@ -20,9 +20,8 @@ const TEST_SUFFIX = '.test.mjs'
20
20
 
21
21
  /**
22
22
  * @typedef {object} JsConcern
23
- * @property {string} name імʼя концерну (`<name>` у `js/<name>/`); для legacy — `'legacy'`
23
+ * @property {string} name імʼя концерну (`<name>` у `js/<name>/`)
24
24
  * @property {string[]} files імена `check*.mjs` у концерні (відсортовані алфавітно)
25
- * @property {boolean} legacy чи це fallback на плаский `js/check.mjs`
26
25
  */
27
26
 
28
27
  /**
@@ -38,8 +37,7 @@ const TEST_SUFFIX = '.test.mjs'
38
37
  */
39
38
 
40
39
  /**
41
- * Перелічує JS-концерни одного правила: підкаталоги `js/<name>/` з принаймні одним `check*.mjs`,
42
- * плюс legacy-fallback на плаский `js/check.mjs` (без підкаталогу).
40
+ * Перелічує JS-концерни одного правила: підкаталоги `js/<name>/` з принаймні одним `check*.mjs`.
43
41
  *
44
42
  * `js/utils/` свідомо пропускається — це хелпери, а не концерни.
45
43
  * @param {string} jsDir абсолютний шлях `rules/<id>/js/`
@@ -49,7 +47,6 @@ async function listJsConcerns(jsDir) {
49
47
  if (!existsSync(jsDir)) return []
50
48
  const topLevel = await readdir(jsDir, { withFileTypes: true })
51
49
 
52
- // Перевага — нова concern-структура (`js/<concern>/check*.mjs`).
53
50
  /** @type {JsConcern[]} */
54
51
  const concerns = []
55
52
  for (const entry of topLevel) {
@@ -59,21 +56,7 @@ async function listJsConcerns(jsDir) {
59
56
  .filter(n => CHECK_FILENAME_RE.test(n) && !n.endsWith(TEST_SUFFIX))
60
57
  .toSorted((a, b) => a.localeCompare(b))
61
58
  if (files.length > 0) {
62
- concerns.push({ name: entry.name, files, legacy: false })
63
- }
64
- }
65
-
66
- // Legacy fallback — лише якщо subdir-концернів немає взагалі. Гібридні правила
67
- // (одночасно legacy check.mjs + нові концерни) трактуються як уже мігровані:
68
- // CLI запускає тільки субдиректорні концерни, flat-файл лишається для backward-compat
69
- // тестів, які імпортують `check` напряму.
70
- if (concerns.length === 0) {
71
- const flatChecks = topLevel
72
- .filter(e => e.isFile() && CHECK_FILENAME_RE.test(e.name) && !e.name.endsWith(TEST_SUFFIX))
73
- .map(e => e.name)
74
- .toSorted((a, b) => a.localeCompare(b))
75
- if (flatChecks.length > 0) {
76
- concerns.push({ name: 'legacy', files: flatChecks, legacy: true })
59
+ concerns.push({ name: entry.name, files })
77
60
  }
78
61
  }
79
62
 
@@ -6,8 +6,6 @@
6
6
  * false — друкуємо `✅ правило не застосовне` і завершуємо без подальших викликів.
7
7
  * 2. **JS-концерни** — кожен `check*.mjs` у `js/<concern>/`. Concern `applies` теж може мати
8
8
  * `check()` для друку контексту (його `applies()` уже відпрацював на кроці 1, він не повторюється).
9
- * Legacy-fallback: плаский `js/check.mjs` лежить як concern `legacy` — імпортується з кореня `js/`,
10
- * а не з підкаталога.
11
9
  * 3. **Policy-концерни** — кожен `policy/<concern>/target.json` через `runConftestBatch`.
12
10
  * Реcолвер `resolveTargetFiles` ділить cache (`walkCache`) між концернами.
13
11
  *
@@ -32,9 +30,7 @@ const APPLIES_CONCERN_NAME = 'applies'
32
30
  * @returns {string} абсолютний шлях
33
31
  */
34
32
  function resolveJsCheckPath(bundledRulesDir, ruleId, concern, fileName) {
35
- return concern.legacy
36
- ? join(bundledRulesDir, ruleId, 'js', fileName)
37
- : join(bundledRulesDir, ruleId, 'js', concern.name, fileName)
33
+ return join(bundledRulesDir, ruleId, 'js', concern.name, fileName)
38
34
  }
39
35
 
40
36
  /**