@nitra/cursor 3.21.1 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
- package/CHANGELOG.md +37 -3
- package/bin/docs/n-cursor.md +636 -0
- package/bin/docs/rename-yaml-extensions.md +207 -0
- package/bin/n-cursor.js +30 -3
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +18 -0
- package/rules/abie/js/docs/applies.md +26 -0
- package/rules/abie/js/docs/env_dns.md +32 -0
- package/rules/abie/js/docs/firebase_hosting.md +23 -0
- package/rules/abie/js/docs/hc_pairing.md +35 -0
- package/rules/abie/js/docs/ua_http_route.md +28 -0
- package/rules/abie/js/docs/ua_node_selector.md +28 -0
- package/rules/abie/lib/docs/enabled.md +29 -0
- package/rules/abie/lib/docs/env-dns.md +35 -0
- package/rules/abie/lib/docs/hc-yaml.md +33 -0
- package/rules/abie/lib/docs/http-route.md +44 -0
- package/rules/abie/lib/docs/k8s-tree.md +40 -0
- package/rules/abie/lib/docs/kustomization-patches.md +47 -0
- package/rules/abie/lib/docs/overlay-paths.md +38 -0
- package/rules/abie/lib/docs/yaml.md +29 -0
- package/rules/adr/docs/fix.md +148 -0
- package/rules/adr/js/docs/hooks.md +259 -0
- package/rules/bun/docs/fix.md +156 -0
- package/rules/bun/js/docs/layout.md +393 -0
- package/rules/capacitor/docs/fix.md +121 -0
- package/rules/capacitor/js/docs/platforms.md +295 -0
- package/rules/changelog/changelog.mdc +2 -2
- package/rules/changelog/docs/fix.md +174 -0
- package/rules/changelog/js/consistency.mjs +114 -13
- package/rules/changelog/js/docs/consistency.md +387 -0
- package/rules/changelog/lib/docs/package-manifest.md +210 -0
- package/rules/ci4/docs/fix.md +179 -0
- package/rules/ci4/js/docs/marksman_config.md +128 -0
- package/rules/docker/docker.mdc +8 -3
- package/rules/docker/docs/fix.md +171 -0
- package/rules/docker/js/docs/lint.md +258 -0
- package/rules/docker/lib/docs/docker-hadolint.md +184 -0
- package/rules/docker/lib/docs/docker-mirror.md +247 -0
- package/rules/docker/lib/docs/docker-native-addon.md +170 -0
- package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
- package/rules/docker/lint/docs/lint.md +193 -0
- package/rules/efes/docs/fix.md +203 -0
- package/rules/feedback/docs/fix.md +140 -0
- package/rules/flow/docs/fix.md +152 -0
- package/rules/ga/docs/fix.md +158 -0
- package/rules/ga/js/docs/lint.md +100 -0
- package/rules/ga/js/docs/workflows.md +217 -0
- package/rules/ga/lint/docs/lint.md +209 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
- package/rules/graphql/docs/fix.md +126 -0
- package/rules/graphql/js/docs/tooling.md +264 -0
- package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
- package/rules/hasura/docs/fix.md +120 -0
- package/rules/hasura/hasura.mdc +14 -0
- package/rules/hasura/js/docs/internal_urls.md +326 -0
- package/rules/image-avif/docs/fix.md +132 -0
- package/rules/image-avif/js/docs/avif_generation.md +241 -0
- package/rules/image-compress/docs/fix.md +150 -0
- package/rules/image-compress/js/docs/package_setup.md +191 -0
- package/rules/js-bun-db/docs/fix.md +148 -0
- package/rules/js-bun-db/js/docs/safety.md +231 -0
- package/rules/js-bun-db/js-bun-db.mdc +42 -13
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
- package/rules/js-bun-redis/docs/fix.md +123 -0
- package/rules/js-bun-redis/js/docs/imports.md +176 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
- package/rules/js-lint/docs/fix.md +117 -0
- package/rules/js-lint/js/docs/lint.md +250 -0
- package/rules/js-lint/js/docs/tooling.md +348 -0
- package/rules/js-lint/js/docs/utils_imports.md +207 -0
- package/rules/js-lint/js/lint-findings.mjs +110 -0
- package/rules/js-lint/js/lint.mjs +86 -15
- package/rules/js-lint-ci/docs/fix.md +154 -0
- package/rules/js-lint-ci/js/docs/lint.md +144 -0
- package/rules/js-mssql/docs/fix.md +128 -0
- package/rules/js-mssql/js/docs/deps.md +263 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
- package/rules/js-run/docs/fix.md +144 -0
- package/rules/js-run/js/docs/runtime.md +388 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
- package/rules/js-run/lib/docs/check-env-scan.md +433 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
- package/rules/k8s/docs/fix.md +129 -0
- package/rules/k8s/js/docs/manifests.md +344 -0
- package/rules/k8s/js/manifests.mjs +6 -2
- package/rules/k8s/k8s.mdc +4 -2
- package/rules/k8s/lint/docs/lint.md +411 -0
- package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
- package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
- package/rules/nginx-default-tpl/docs/fix.md +124 -0
- package/rules/nginx-default-tpl/js/docs/template.md +378 -0
- package/rules/npm-module/docs/fix.md +98 -0
- package/rules/npm-module/js/docs/package_structure.md +274 -0
- package/rules/npm-module/js/docs/rule_meta.md +137 -0
- package/rules/npm-module/js/docs/skill_meta.md +190 -0
- package/rules/php/docs/fix.md +107 -0
- package/rules/php/js/docs/tooling.md +152 -0
- package/rules/php/lint/docs/lint.md +215 -0
- package/rules/python/docs/fix.md +163 -0
- package/rules/python/js/docs/applies.md +108 -0
- package/rules/python/js/docs/tooling.md +153 -0
- package/rules/python/lint/docs/lint.md +322 -0
- package/rules/rego/docs/fix.md +121 -0
- package/rules/rego/js/docs/applies.md +174 -0
- package/rules/rego/js/docs/lint.md +118 -0
- package/rules/rego/lint/docs/lint.md +204 -0
- package/rules/release/docs/change.md +185 -0
- package/rules/release/docs/fix.md +119 -0
- package/rules/release/docs/release.md +222 -0
- package/rules/release/lib/docs/aggregate.md +246 -0
- package/rules/release/lib/docs/change-file.md +200 -0
- package/rules/release/lib/docs/fallback.md +203 -0
- package/rules/rust/docs/fix.md +129 -0
- package/rules/rust/js/docs/applies.md +140 -0
- package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
- package/rules/security/docs/fix.md +86 -0
- package/rules/security/js/docs/lint.md +171 -0
- package/rules/security/js/docs/sample_secret.md +190 -0
- package/rules/security/js/docs/trufflehog.md +137 -0
- package/rules/security/js/lint.mjs +9 -1
- package/rules/style-lint/docs/fix.md +155 -0
- package/rules/style-lint/js/docs/lint.md +184 -0
- package/rules/style-lint/js/docs/tooling.md +194 -0
- package/rules/tauri/docs/fix.md +158 -0
- package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
- package/rules/tauri/js/docs/tooling.md +228 -0
- package/rules/test/coverage/coverage.mjs +15 -3
- package/rules/test/docs/fix.md +132 -0
- package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
- package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
- package/rules/test/js/docs/cargo_mutants_config.md +173 -0
- package/rules/test/js/docs/location.md +136 -0
- package/rules/test/js/docs/no-process-chdir.md +160 -0
- package/rules/test/js/docs/no-relative-fs-path.md +271 -0
- package/rules/test/js/docs/stryker_config.md +152 -0
- package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
- package/rules/text/docs/fix.md +118 -0
- package/rules/text/js/docs/forbidden-prettier.md +143 -0
- package/rules/text/js/docs/formatting.md +256 -0
- package/rules/text/js/docs/lint.md +122 -0
- package/rules/text/lint/docs/lint.md +220 -0
- package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
- package/rules/text/lint/docs/run-shellcheck.md +212 -0
- package/rules/text/lint/docs/run-v8r.md +197 -0
- package/rules/vue/docs/fix.md +127 -0
- package/rules/vue/js/docs/packages.md +335 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
- package/rules/worktree/docs/fix.md +161 -0
- package/schemas/rule-meta.json +5 -1
- package/scripts/auto-rules.mjs +7 -4
- package/scripts/coverage-classify/docs/apply.md +202 -0
- package/scripts/coverage-classify/docs/cache.md +203 -0
- package/scripts/coverage-classify/docs/index.md +218 -0
- package/scripts/coverage-classify/docs/prompt.md +132 -0
- package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
- package/scripts/coverage-fix-extract.mjs +122 -0
- package/scripts/coverage-fix.mjs +1 -1
- package/scripts/dispatcher/docs/graph.md +346 -0
- package/scripts/dispatcher/docs/index.md +236 -0
- package/scripts/dispatcher/docs/trace.md +296 -0
- package/scripts/dispatcher/index.mjs +1 -1
- package/scripts/dispatcher/lib/active.mjs +4 -8
- package/scripts/dispatcher/lib/commands.mjs +7 -11
- package/scripts/dispatcher/lib/docs/active.md +348 -0
- package/scripts/dispatcher/lib/docs/artifact.md +232 -0
- package/scripts/dispatcher/lib/docs/budget.md +167 -0
- package/scripts/dispatcher/lib/docs/capability.md +196 -0
- package/scripts/dispatcher/lib/docs/commands.md +210 -0
- package/scripts/dispatcher/lib/docs/events.md +182 -0
- package/scripts/dispatcher/lib/docs/executor.md +190 -0
- package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
- package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
- package/scripts/dispatcher/lib/docs/gate.md +231 -0
- package/scripts/dispatcher/lib/docs/level.md +335 -0
- package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
- package/scripts/dispatcher/lib/docs/plan.md +200 -0
- package/scripts/dispatcher/lib/docs/planner.md +269 -0
- package/scripts/dispatcher/lib/docs/review.md +255 -0
- package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
- package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
- package/scripts/dispatcher/lib/docs/spec.md +203 -0
- package/scripts/dispatcher/lib/docs/state-store.md +303 -0
- package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
- package/scripts/dispatcher/lib/executor.mjs +6 -1
- package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
- package/scripts/dispatcher/lib/level.mjs +29 -3
- package/scripts/dispatcher/lib/review.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
- package/scripts/docs/auto-rules.md +376 -0
- package/scripts/docs/auto-skills.md +173 -0
- package/scripts/docs/build-agents-commands.md +183 -0
- package/scripts/docs/cli-entry.md +153 -0
- package/scripts/docs/coverage-fix.md +177 -0
- package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
- package/scripts/lib/changed-files.mjs +4 -1
- package/scripts/lib/diff-added-lines.mjs +85 -0
- package/scripts/lib/docs/changed-files.md +149 -0
- package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
- package/scripts/lib/docs/check-reporter.md +175 -0
- package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
- package/scripts/lib/docs/discover-checkable-rules.md +165 -0
- package/scripts/lib/docs/ensure-tool.md +254 -0
- package/scripts/lib/docs/generated-markdown.md +275 -0
- package/scripts/lib/docs/gha-workflow.md +326 -0
- package/scripts/lib/docs/inline-template-links.md +303 -0
- package/scripts/lib/docs/list-rule-ids.md +156 -0
- package/scripts/lib/docs/load-cursor-config.md +147 -0
- package/scripts/lib/docs/mirror-parity.md +167 -0
- package/scripts/lib/worktree.mjs +26 -0
- package/scripts/worktree-cli.mjs +12 -2
- package/skills/coverage-fix/SKILL.md +34 -45
- package/skills/docgen/SKILL.md +44 -23
- package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
- package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
- package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
- package/skills/docgen/js/docgen-ignore.mjs +54 -0
- package/skills/docgen/js/docgen-scan.mjs +37 -21
- package/skills/llm-patch/SKILL.md +23 -2
- package/skills/start-check/SKILL.md +26 -53
- package/skills/start-check/js/check.mjs +211 -0
- package/skills/taze/SKILL.md +9 -3
- package/skills/taze/js/diff.mjs +154 -0
- package/types/bin/n-cursor.d.ts +1 -1
- package/skills/fix-tests/SKILL.md +0 -119
- package/skills/fix-tests/meta.json +0 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# auto-rules.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `npm/scripts/auto-rules.mjs` — це **движок автодетекту правил** для конфігурації
|
|
6
|
+
`.n-cursor.json`. Його задача — за метаданими з `npm/rules/<id>/meta.json` і за станом
|
|
7
|
+
проєкту-користувача (вміст файлів та структура дерева) вирахувати, які правила слід
|
|
8
|
+
автоматично активувати в конфізі CLI `n-cursor`.
|
|
9
|
+
|
|
10
|
+
Архітектура data-driven: список правил, порядок і граф залежностей **не зашиваються в код**
|
|
11
|
+
— вони виводяться з `meta.json` кожного правила в `npm/rules/<id>/`. Кожне правило в `meta.json`
|
|
12
|
+
має поле `auto` з однією зі специфікацій активації (`always`, `glob`, `predicate`, `rules`).
|
|
13
|
+
|
|
14
|
+
Основні відповідальності модуля:
|
|
15
|
+
|
|
16
|
+
- **Discovery**: сканування `npm/rules/<id>/meta.json` і побудова мапи `RULE_AUTO_ACTIVATION`
|
|
17
|
+
з нормалізованими spec-ами автоактивації (`discoverRuleAutoActivation`).
|
|
18
|
+
- **Експорт стабільного порядку правил**: алфавітний `AUTO_RULE_ORDER` замість хардкод-масиву.
|
|
19
|
+
- **Експорт графа залежностей**: `AUTO_RULE_DEPENDENCIES` із spec-ів типу `rules`.
|
|
20
|
+
- **Збір content-фактів**: обхід дерева репо й збір ознак (`hasBunSqlImport`,
|
|
21
|
+
`hasGqlTaggedTemplates`, `hasHasuraConfig`, `hasRegoFile`, `hasTempoDir`)
|
|
22
|
+
через `collectAutoRuleFacts` — для зворотної сумісності з прямими читачами та для
|
|
23
|
+
предикатів типу `gqlTaggedTemplate`/`hasuraConfigMarker`/`jsBunDbSignal`.
|
|
24
|
+
- **Збір relative-posix-шляхів**: `collectRepoPaths` (повертає і файли, і каталоги — для
|
|
25
|
+
glob-матчингу, який може цілитися в порожні директорії типу `npm`, `k8s`, `.github/workflows`).
|
|
26
|
+
- **Обчислення активних правил**: `detectAutoRules` запускає `specMatches` для кожного
|
|
27
|
+
правила (з пропуском типу `rules`), потім транзитивно розгортає залежності через
|
|
28
|
+
`resolveRuleDependencies` і повертає id у стабільному порядку.
|
|
29
|
+
- **Злиття з конфігом**: `mergeConfigWithAutoDetected` лише **додає** в `rules`/`skills`
|
|
30
|
+
виявлені id, поважаючи `disable-rules`/`disable-skills` й нормалізацію legacy-id
|
|
31
|
+
через `migrateRuleIds`.
|
|
32
|
+
|
|
33
|
+
Автодетект **скілів** (не правил) — у сусідньому модулі `./auto-skills.mjs`; цей файл
|
|
34
|
+
лише приймає вже виявлені `detectedSkills` у `mergeConfigWithAutoDetected`.
|
|
35
|
+
|
|
36
|
+
## Експорти / API
|
|
37
|
+
|
|
38
|
+
Реекспорти з `./lib/rule-meta-helpers.mjs` (для одного публічного entry-point):
|
|
39
|
+
|
|
40
|
+
- `detectLegacyRuleIds`
|
|
41
|
+
- `getRepositoryUrl`
|
|
42
|
+
- `isMonorepoPackage`
|
|
43
|
+
- `migrateRuleIds`
|
|
44
|
+
- `normalizeIdList`
|
|
45
|
+
- `RULE_MIGRATIONS`
|
|
46
|
+
|
|
47
|
+
Власні експорти модуля:
|
|
48
|
+
|
|
49
|
+
| Експорт | Тип | Призначення |
|
|
50
|
+
| ----------------------------- | ------------------------------------------- | ---------------------------------------------------------------------- |
|
|
51
|
+
| `discoverRuleAutoActivation` | `function` | Скан `npm/rules/<id>/meta.json` → мапа id → `RuleAutoSpec`. |
|
|
52
|
+
| `AUTO_RULE_ORDER` | `readonly string[]` (frozen) | Алфавітний порядок усіх правил із розпізнаним `auto`. |
|
|
53
|
+
| `AUTO_RULE_DEPENDENCIES` | `readonly Record<string,string[]>` (frozen) | Граф залежностей із spec-ів типу `rules`. |
|
|
54
|
+
| `collectAutoRuleFacts` | `async function` | Обхід дерева й збір content-фактів. |
|
|
55
|
+
| `detectAutoRules` | `async function` | Головна функція: повертає `{ rules: string[] }` за `meta.json` правил. |
|
|
56
|
+
| `mergeConfigWithAutoDetected` | `function` | Зливає виявлені rules+skills у конфіг із поправкою на legacy-id. |
|
|
57
|
+
|
|
58
|
+
Внутрішні (не експортовані):
|
|
59
|
+
|
|
60
|
+
- `sourceContentHasBunSqlImport`
|
|
61
|
+
- `shouldScanFileForGql`, `updateGqlFactFromFile`
|
|
62
|
+
- `shouldScanFileForBunSql`, `updateBunSqlFactFromFile`
|
|
63
|
+
- `updateHasuraFactFromFile`
|
|
64
|
+
- `processFileEntry`
|
|
65
|
+
- `collectRepoPaths`
|
|
66
|
+
- `resolveRuleDependencies`
|
|
67
|
+
- `specMatches`
|
|
68
|
+
|
|
69
|
+
Внутрішні константи:
|
|
70
|
+
|
|
71
|
+
- `PACKAGE_ROOT` — корінь пакету (`dirname(dirname(fileURLToPath(import.meta.url)))`).
|
|
72
|
+
- `RULES_DIR` — `${PACKAGE_ROOT}/rules`.
|
|
73
|
+
- `RULE_AUTO_ACTIVATION` — результат `discoverRuleAutoActivation()`, обчислюється на load-time.
|
|
74
|
+
- `HASURA_CONFIG_MARKER = 'metadata_directory: metadata'`.
|
|
75
|
+
- `REGO_RE = /\.rego$/iu`.
|
|
76
|
+
- `IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])`.
|
|
77
|
+
- `DEFAULT_DISABLED_LIST = Object.freeze([])`.
|
|
78
|
+
|
|
79
|
+
## Функції
|
|
80
|
+
|
|
81
|
+
### `discoverRuleAutoActivation(rulesDir = RULES_DIR)`
|
|
82
|
+
|
|
83
|
+
- **Сигнатура**: `(rulesDir?: string) => Record<string, RuleAutoSpec>`
|
|
84
|
+
- **Параметри**:
|
|
85
|
+
- `rulesDir` — override кореня `rules/` (для тестів). За замовчуванням — `RULES_DIR`.
|
|
86
|
+
- **Повертає**: мапу `id → RuleAutoSpec` (лише правила, де `parseRuleAutoSpec(raw.auto)`
|
|
87
|
+
повернув не-null).
|
|
88
|
+
- **Side effects**: синхронне читання `readdirSync` й читання `meta.json` кожного підкаталогу
|
|
89
|
+
через `readRuleMetaRaw`. Не модифікує файли.
|
|
90
|
+
- **Обробка помилок**: якщо `rulesDir` недоступний — повертає порожній обʼєкт. Пропускає
|
|
91
|
+
директорії з імʼям, що починається на `.`, і не-директорії.
|
|
92
|
+
|
|
93
|
+
### `AUTO_RULE_ORDER` / `AUTO_RULE_DEPENDENCIES`
|
|
94
|
+
|
|
95
|
+
Не функції — обчислюються одноразово на load-time:
|
|
96
|
+
|
|
97
|
+
- `AUTO_RULE_ORDER = Object.freeze(Object.keys(RULE_AUTO_ACTIVATION).toSorted(localeCompare))`
|
|
98
|
+
- `AUTO_RULE_DEPENDENCIES = Object.freeze(Object.fromEntries(...))` — будується з
|
|
99
|
+
entries, де `'rules' in spec` (тобто spec типу C — звичайне посилання на залежні правила).
|
|
100
|
+
Значення кожного ключа — заморожений масив id залежностей.
|
|
101
|
+
|
|
102
|
+
### `sourceContentHasBunSqlImport(content, relativePath)`
|
|
103
|
+
|
|
104
|
+
- **Сигнатура**: `(content: string, relativePath: string) => boolean`
|
|
105
|
+
- **Параметри**:
|
|
106
|
+
- `content` — повний вміст файлу.
|
|
107
|
+
- `relativePath` — шлях posix відносно кореня репо.
|
|
108
|
+
- **Повертає**: `true`, якщо в тексті є `import { sql }` або `import { SQL }` з `"bun"`.
|
|
109
|
+
- **Side effects**: жодних.
|
|
110
|
+
- **Логіка**: викликає `contentForVueImportScan(content, relativePath)` (витягує `<script>`
|
|
111
|
+
для `.vue`) і передає в `textHasBunSqlImport`.
|
|
112
|
+
|
|
113
|
+
### `shouldScanFileForGql(relPath, facts)`
|
|
114
|
+
|
|
115
|
+
- **Сигнатура**: `(relPath: string, facts: { hasGqlTaggedTemplates: boolean }) => boolean`
|
|
116
|
+
- **Повертає**: `true`, лише якщо факт ще не встановлено і файл — підходяще джерело
|
|
117
|
+
(`isGqlScanSourceFile`) та не входить у виключення (`shouldSkipFileForGqlScan`).
|
|
118
|
+
- **Side effects**: жодних.
|
|
119
|
+
|
|
120
|
+
### `updateGqlFactFromFile(absPath, relPath, facts)`
|
|
121
|
+
|
|
122
|
+
- **Сигнатура**: `async (absPath: string, relPath: string, facts: { hasGqlTaggedTemplates: boolean }) => Promise<void>`
|
|
123
|
+
- **Side effects**: читає файл (`readFile utf8`), мутує `facts.hasGqlTaggedTemplates = true`,
|
|
124
|
+
якщо `sourceFileHasGqlTaggedTemplate` дав true.
|
|
125
|
+
- **Обробка помилок**: `try/catch` — пошкоджені/недоступні файли мовчки ігноруються.
|
|
126
|
+
|
|
127
|
+
### `shouldScanFileForBunSql(relPath, facts)`
|
|
128
|
+
|
|
129
|
+
- **Сигнатура**: `(relPath: string, facts: { hasBunSqlImport: boolean }) => boolean`
|
|
130
|
+
- **Повертає**: `true`, якщо факт ще не встановлено і файл-кандидат (ті самі правила,
|
|
131
|
+
що для gql: `isGqlScanSourceFile` + `!shouldSkipFileForGqlScan`).
|
|
132
|
+
- **Side effects**: жодних.
|
|
133
|
+
|
|
134
|
+
### `updateBunSqlFactFromFile(absPath, relPath, facts)`
|
|
135
|
+
|
|
136
|
+
- **Сигнатура**: `async (absPath: string, relPath: string, facts: { hasBunSqlImport: boolean }) => Promise<void>`
|
|
137
|
+
- **Side effects**: читає файл, мутує `facts.hasBunSqlImport = true` за позитивної відповіді
|
|
138
|
+
`sourceContentHasBunSqlImport`.
|
|
139
|
+
- **Обробка помилок**: `try/catch` — мовчки ігнорує помилки I/O.
|
|
140
|
+
|
|
141
|
+
### `updateHasuraFactFromFile(absPath, fileName, facts)`
|
|
142
|
+
|
|
143
|
+
- **Сигнатура**: `async (absPath: string, fileName: string, facts: { hasHasuraConfig: boolean }) => Promise<void>`
|
|
144
|
+
- **Логіка**: якщо факт уже встановлений або `fileName !== 'config.yaml'` — ранній вихід.
|
|
145
|
+
Інакше читає файл і мутує `facts.hasHasuraConfig = true`, якщо вміст містить
|
|
146
|
+
підрядок `'metadata_directory: metadata'` (`HASURA_CONFIG_MARKER`).
|
|
147
|
+
- **Side effects**: один `readFile`, мутація `facts`.
|
|
148
|
+
- **Обробка помилок**: `try/catch` — мовчки ігнорує.
|
|
149
|
+
|
|
150
|
+
### `processFileEntry(absPath, root, facts)`
|
|
151
|
+
|
|
152
|
+
- **Сигнатура**: `async (absPath: string, root: string, facts: AutoRuleFacts) => Promise<void>`
|
|
153
|
+
- **Параметри**:
|
|
154
|
+
- `absPath` — абсолютний шлях файлу.
|
|
155
|
+
- `root` — абсолютний корінь репо.
|
|
156
|
+
- `facts` — обʼєкт `{ hasBunSqlImport, hasGqlTaggedTemplates, hasHasuraConfig, hasRegoFile }`.
|
|
157
|
+
- **Логіка** для одного файлу:
|
|
158
|
+
1. Обчислює `rel = relative(root, absPath)` з нормалізацією `\\` → `/`.
|
|
159
|
+
2. Якщо шлях матчить `\.rego$` — встановлює `facts.hasRegoFile = true`.
|
|
160
|
+
3. Якщо `shouldScanFileForGql` — викликає `updateGqlFactFromFile`.
|
|
161
|
+
4. Якщо `shouldScanFileForBunSql` — викликає `updateBunSqlFactFromFile`.
|
|
162
|
+
5. Завжди викликає `updateHasuraFactFromFile` (вона сама перевірить `fileName`).
|
|
163
|
+
- **Side effects**: до 3 потенційних `readFile` + мутації `facts`.
|
|
164
|
+
|
|
165
|
+
### `collectAutoRuleFacts(root)` — **експортована**
|
|
166
|
+
|
|
167
|
+
- **Сигнатура**: `async (root: string) => Promise<{ hasBunSqlImport: boolean, hasGqlTaggedTemplates: boolean, hasHasuraConfig: boolean, hasRegoFile: boolean, hasTempoDir: boolean }>`
|
|
168
|
+
- **Параметри**: `root` — абсолютний шлях кореня репо.
|
|
169
|
+
- **Повертає**: Promise з обʼєктом content-фактів.
|
|
170
|
+
- **Логіка**:
|
|
171
|
+
1. Ініціалізує `facts` усіма `false`.
|
|
172
|
+
2. Внутрішня рекурсія `walk(dir)` через `readdir({ withFileTypes: true })`:
|
|
173
|
+
- якщо `entry.isDirectory()` і імʼя не в `IGNORED_DIR_NAMES`:
|
|
174
|
+
- якщо `entry.name === 'tempo'` — встановлює `facts.hasTempoDir = true`;
|
|
175
|
+
- рекурсивний `await walk(absPath)`;
|
|
176
|
+
- якщо `entry.isFile()` — `await processFileEntry(absPath, root, facts)`.
|
|
177
|
+
3. Помилки `readdir` мовчки призводять до `return` (пропускаємо каталог).
|
|
178
|
+
- **Side effects**: рекурсивний обхід FS, читання вмісту файлів-кандидатів.
|
|
179
|
+
- **Зворотна сумісність**: `hasRegoFile`/`hasTempoDir` лишаються для прямих читачів
|
|
180
|
+
(тести, зовнішній код), хоча активація відповідних правил уже data-driven.
|
|
181
|
+
|
|
182
|
+
### `collectRepoPaths(root)` — внутрішня
|
|
183
|
+
|
|
184
|
+
- **Сигнатура**: `async (root: string) => Promise<string[]>`
|
|
185
|
+
- **Повертає**: масив relative-posix-шляхів **і файлів, і каталогів**.
|
|
186
|
+
- **Чому й каталоги**: частина glob-спеків указує на самі директорії (`npm`, `k8s`,
|
|
187
|
+
`.github/workflows`), які можуть бути порожніми — без цього правила `npm-module`, `k8s`,
|
|
188
|
+
`ga` не активовувалися б на дереві без файлів усередині.
|
|
189
|
+
- **Логіка**: внутрішня `walk(dir)` через `readdir({ withFileTypes: true })`. На директорії
|
|
190
|
+
(не в `IGNORED_DIR_NAMES`) — пушить шлях і рекурсує. На файл — пушить шлях. Помилки
|
|
191
|
+
`readdir` мовчки повертають з гілки.
|
|
192
|
+
|
|
193
|
+
### `resolveRuleDependencies(detectedRules, addRule)`
|
|
194
|
+
|
|
195
|
+
- **Сигнатура**: `(detectedRules: string[], addRule: (ruleId: string) => void) => void`
|
|
196
|
+
- **Параметри**:
|
|
197
|
+
- `detectedRules` — мутабельний список вже зібраних id (мутується через `addRule`).
|
|
198
|
+
- `addRule` — callback з фабрики, що поважає `disable-rules` й дублі.
|
|
199
|
+
- **Логіка**: fixed-point loop — повторно проходить усіма парами `[ruleId, deps]` з
|
|
200
|
+
`AUTO_RULE_DEPENDENCIES`. На кожному проході: якщо правило ще не в детекті й **усі**
|
|
201
|
+
залежності вже в детекті — викликає `addRule(ruleId)` і, якщо довжина зросла,
|
|
202
|
+
встановлює `changed = true` для наступної ітерації.
|
|
203
|
+
- **Гарантія**: дозволяє транзитивні ланцюги `a → b → c` незалежно від порядку оголошення
|
|
204
|
+
в meta-файлах.
|
|
205
|
+
- **Side effects**: викликає переданий `addRule` (який мутує `detectedRules`).
|
|
206
|
+
|
|
207
|
+
### `specMatches(spec, ctx)`
|
|
208
|
+
|
|
209
|
+
- **Сигнатура**: `async (spec: RuleAutoSpec, ctx: { root: string, facts: object, paths: string[], packageJsonParsed: unknown }) => Promise<boolean>`
|
|
210
|
+
- **Логіка** — диспетчинг за дискримінантним ключем spec:
|
|
211
|
+
- `'always' in spec` → `true` безумовно.
|
|
212
|
+
- `'glob' in spec` → конвертує кожен glob у regex через `globToRegex`; повертає `true`,
|
|
213
|
+
якщо **будь-який** шлях у `ctx.paths` матчить **будь-який** regex.
|
|
214
|
+
- `'predicate' in spec` → шукає функцію `RULE_PREDICATES[spec.predicate]`. Якщо не знайдено
|
|
215
|
+
— `false`. Інакше викликає за іменем предиката з різними сигнатурами:
|
|
216
|
+
- `repoUrlMarker` → `fn(ctx.packageJsonParsed, spec.arg)` — читає корений `package.json`
|
|
217
|
+
- arg-маркер.
|
|
218
|
+
- `gqlTaggedTemplate` або `hasuraConfigMarker` → `fn(ctx.facts)` — content-факти.
|
|
219
|
+
- `jsBunDbSignal` → `fn(ctx.root, ctx.facts)` — комбінований сигнал.
|
|
220
|
+
- інші (`depInAnyPackageJson`, `nestedPackageWithoutVite`) → `fn(ctx.root, spec.arg)`.
|
|
221
|
+
- Якщо жодна гілка не спрацювала — `false`.
|
|
222
|
+
- **Side effects**: залежать від конкретного предиката (можуть читати FS).
|
|
223
|
+
|
|
224
|
+
### `detectAutoRules({ root, availableRules, packageJsonParsed, disableRules })` — **експортована**
|
|
225
|
+
|
|
226
|
+
- **Сигнатура**: `async (params: { root: string, availableRules: string[], packageJsonParsed: unknown, disableRules?: string[] }) => Promise<{ rules: string[] }>`
|
|
227
|
+
- **Параметри**:
|
|
228
|
+
- `root` — абсолютний корінь репо-аналізованого проєкту.
|
|
229
|
+
- `availableRules` — перелік доступних правил із пакету `n-cursor`.
|
|
230
|
+
- `packageJsonParsed` — розпарсений кореневий `package.json` користувача (або `null`).
|
|
231
|
+
- `disableRules` — список з конфігу (за замовчуванням `DEFAULT_DISABLED_LIST = []`).
|
|
232
|
+
- **Повертає**: `{ rules: string[] }` — id у стабільному порядку `AUTO_RULE_ORDER`.
|
|
233
|
+
- **Логіка**:
|
|
234
|
+
1. `facts = await collectAutoRuleFacts(root)` — content-факти.
|
|
235
|
+
2. `paths = await collectRepoPaths(root)` — relative-posix-шляхи для glob.
|
|
236
|
+
3. `normalizedRules` — `Set` доступних id (lower-case, trim) — для перевірки доступності.
|
|
237
|
+
4. `disableRulesSet` — `Set` з `disableRules`.
|
|
238
|
+
5. `detectedRules: string[] = []` + локальна функція `addRule(ruleId)`:
|
|
239
|
+
- пропускає id, якщо його **немає в `normalizedRules`**, або є в `disableRulesSet`,
|
|
240
|
+
або вже в `detectedRules`;
|
|
241
|
+
- інакше пушить.
|
|
242
|
+
6. Перший прохід — над `Object.entries(RULE_AUTO_ACTIVATION)`:
|
|
243
|
+
- пропускає spec-и типу `rules` (вони обробляються в наступному кроці);
|
|
244
|
+
- для решти — `await specMatches(spec, { root, facts, paths, packageJsonParsed })`;
|
|
245
|
+
- якщо true — `addRule(ruleId)`.
|
|
246
|
+
7. `resolveRuleDependencies(detectedRules, addRule)` — транзитивне розгортання.
|
|
247
|
+
8. Фінальне `rules = AUTO_RULE_ORDER.filter(r => detectedRules.includes(r))` — стабільний порядок.
|
|
248
|
+
- **Side effects**: повний обхід дерева репо (двічі — у `collectAutoRuleFacts` і
|
|
249
|
+
`collectRepoPaths`) + читання вмісту файлів-кандидатів + потенційні читання з боку
|
|
250
|
+
предикатів.
|
|
251
|
+
|
|
252
|
+
### `mergeConfigWithAutoDetected({ config, detectedRules, detectedSkills })` — **експортована**
|
|
253
|
+
|
|
254
|
+
- **Сигнатура**: `(params: { config: { rules: unknown, skills?: unknown, ['disable-rules']?: unknown, ['disable-skills']?: unknown }, detectedRules: string[], detectedSkills: string[] }) => { rules: string[], skills: string[] } & Record<string, unknown>`
|
|
255
|
+
- **Логіка**:
|
|
256
|
+
1. `existingRules = migrateRuleIds(normalizeIdList(config.rules))` — нормалізує і
|
|
257
|
+
мігрує legacy-id.
|
|
258
|
+
2. `existingSkills = normalizeIdList(config.skills)`.
|
|
259
|
+
3. `disableRules = migrateRuleIds(normalizeIdList(config['disable-rules']))`.
|
|
260
|
+
4. `disableSkills = normalizeIdList(config['disable-skills'])`.
|
|
261
|
+
5. Будує `rules = [...existingRules]`, додає кожен `detectedRules[i]`, якщо його ще
|
|
262
|
+
немає в `rules` **і** він не в `disableRules`.
|
|
263
|
+
6. Аналогічно для `skills` (з `disableSkills`).
|
|
264
|
+
7. `normalized = { rules, skills }`. Додає `'disable-rules'`/`'disable-skills'` лише
|
|
265
|
+
якщо вони не порожні.
|
|
266
|
+
- **Семантика**: **не прибирає** елементи, що були в конфізі вручну (idempotent додавання).
|
|
267
|
+
- **Side effects**: жодних — pure-функція над переданим конфігом.
|
|
268
|
+
|
|
269
|
+
## Залежності
|
|
270
|
+
|
|
271
|
+
### Сторонні / Node.js (stdlib)
|
|
272
|
+
|
|
273
|
+
- `node:fs` → `readdirSync` (для синхронного скану `rules/` на load-time).
|
|
274
|
+
- `node:fs/promises` → `readdir`, `readFile` (асинхронний обхід проєкту й читання вмісту).
|
|
275
|
+
- `node:path` → `basename`, `dirname`, `join`, `relative` (нормалізація шляхів).
|
|
276
|
+
- `node:url` → `fileURLToPath` (резолв `PACKAGE_ROOT` з `import.meta.url`).
|
|
277
|
+
|
|
278
|
+
### Внутрішні модулі пакету
|
|
279
|
+
|
|
280
|
+
- `../rules/npm-module/js/package_structure.mjs` → `globToRegex` — конвертація glob у regex
|
|
281
|
+
для spec типу `glob`.
|
|
282
|
+
- `../rules/js-bun-db/lib/bun-sql-scan.mjs` → `textHasBunSqlImport` — детекція `import { sql }`
|
|
283
|
+
з `"bun"` у текстовому вмісті.
|
|
284
|
+
- `../rules/graphql/lib/graphql-gql-scan.mjs` → `isGqlScanSourceFile`,
|
|
285
|
+
`shouldSkipFileForGqlScan`, `sourceFileHasGqlTaggedTemplate` — політика сканування для gql.
|
|
286
|
+
- `../rules/vue/lib/vue-forbidden-imports.mjs` → `contentForVueImportScan` — витягування
|
|
287
|
+
`<script>` для `.vue` перед сканом імпортів.
|
|
288
|
+
- `./lib/rule-meta.mjs` → `parseRuleAutoSpec`, `readRuleMetaRaw` — парсинг `meta.json` правил.
|
|
289
|
+
- `./lib/rule-meta-helpers.mjs` → `migrateRuleIds`, `normalizeIdList` (використання) +
|
|
290
|
+
реекспорти `detectLegacyRuleIds`, `getRepositoryUrl`, `isMonorepoPackage`, `RULE_MIGRATIONS`.
|
|
291
|
+
- `./lib/rule-predicates.mjs` → `RULE_PREDICATES` — реєстр незводимих предикатів за іменами.
|
|
292
|
+
|
|
293
|
+
### Дотичні (не імпортуються прямо, але семантично повʼязані)
|
|
294
|
+
|
|
295
|
+
- `./auto-skills.mjs` — автодетект скілів; результати приймає `mergeConfigWithAutoDetected`
|
|
296
|
+
через `detectedSkills`.
|
|
297
|
+
- `npm/rules/<id>/meta.json` — джерело даних для `discoverRuleAutoActivation`.
|
|
298
|
+
|
|
299
|
+
## Потік виконання / Використання
|
|
300
|
+
|
|
301
|
+
### Сценарій 1: автогенерація `.n-cursor.json`
|
|
302
|
+
|
|
303
|
+
Очікувана послідовність викликача (наприклад, CLI `n-cursor init`):
|
|
304
|
+
|
|
305
|
+
1. Зчитати поточний `.n-cursor.json` користувача (або порожній обʼєкт).
|
|
306
|
+
2. Зчитати кореневий `package.json` користувача (parse → `packageJsonParsed`).
|
|
307
|
+
3. Отримати список доступних правил із пакету (наприклад, із `npm/rules/*/`).
|
|
308
|
+
4. Викликати `const { rules } = await detectAutoRules({ root, availableRules, packageJsonParsed, disableRules })`.
|
|
309
|
+
5. Викликати `detectAutoSkills(...)` з `./auto-skills.mjs` (поза цим файлом) → `detectedSkills`.
|
|
310
|
+
6. `const normalized = mergeConfigWithAutoDetected({ config, detectedRules: rules, detectedSkills })`.
|
|
311
|
+
7. Записати `normalized` назад у `.n-cursor.json`.
|
|
312
|
+
|
|
313
|
+
### Сценарій 2: програмне читання фактів
|
|
314
|
+
|
|
315
|
+
Якщо потрібно лише сирі content-факти (наприклад, для іншого скрипта):
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
import { collectAutoRuleFacts } from 'n-cursor/scripts/auto-rules.mjs'
|
|
319
|
+
const facts = await collectAutoRuleFacts(process.cwd())
|
|
320
|
+
// facts.hasBunSqlImport, facts.hasGqlTaggedTemplates, facts.hasHasuraConfig,
|
|
321
|
+
// facts.hasRegoFile, facts.hasTempoDir
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Сценарій 3: інспекція реєстру правил
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
import { discoverRuleAutoActivation, AUTO_RULE_ORDER, AUTO_RULE_DEPENDENCIES } from 'n-cursor/scripts/auto-rules.mjs'
|
|
328
|
+
const reg = discoverRuleAutoActivation() // мапа id → spec
|
|
329
|
+
const order = AUTO_RULE_ORDER // алфавітний список усіх правил із auto
|
|
330
|
+
const deps = AUTO_RULE_DEPENDENCIES // граф залежностей (frozen)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Послідовність всередині `detectAutoRules`
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
collectAutoRuleFacts(root) ──┐
|
|
337
|
+
├─→ ctx = { root, facts, paths, packageJsonParsed }
|
|
338
|
+
collectRepoPaths(root) ──────┘
|
|
339
|
+
│
|
|
340
|
+
▼
|
|
341
|
+
для кожного [ruleId, spec] у RULE_AUTO_ACTIVATION (крім spec типу 'rules'):
|
|
342
|
+
specMatches(spec, ctx) → addRule(ruleId)
|
|
343
|
+
│
|
|
344
|
+
▼
|
|
345
|
+
resolveRuleDependencies(detectedRules, addRule) // fixed-point loop
|
|
346
|
+
│
|
|
347
|
+
▼
|
|
348
|
+
rules = AUTO_RULE_ORDER.filter(r => detectedRules.includes(r))
|
|
349
|
+
│
|
|
350
|
+
▼
|
|
351
|
+
return { rules }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Інваріанти
|
|
355
|
+
|
|
356
|
+
- **Стабільний порядок**: фінальний `rules` завжди впорядкований за `AUTO_RULE_ORDER`
|
|
357
|
+
(алфавіт). Це гарантує детермінованість для diff-ів конфігу.
|
|
358
|
+
- **Поважання `disable-rules`**: правило, явно вимкнене користувачем, не зʼявиться навіть
|
|
359
|
+
якщо його spec матчить — фільтр у `addRule`.
|
|
360
|
+
- **Тільки додавання**: `mergeConfigWithAutoDetected` ніколи не видаляє вручну задані
|
|
361
|
+
елементи (idempotent).
|
|
362
|
+
- **Зворотна сумісність**: `hasRegoFile`/`hasTempoDir` лишаються в експортуваному
|
|
363
|
+
обʼєкті `collectAutoRuleFacts` навіть якщо вже не використовуються у власному
|
|
364
|
+
диспетчингу — для прямих читачів.
|
|
365
|
+
- **Тиха толерантність до помилок FS**: `try/catch` навколо `readFile`/`readdir` —
|
|
366
|
+
пошкоджені/недоступні файли не валять детект.
|
|
367
|
+
|
|
368
|
+
### Архітектурні нотатки
|
|
369
|
+
|
|
370
|
+
- **Load-time скан**: `RULE_AUTO_ACTIVATION = discoverRuleAutoActivation()` виконується
|
|
371
|
+
**при імпорті модуля** (синхронно через `readdirSync`). Це означає, що зміни в
|
|
372
|
+
`npm/rules/<id>/meta.json` після старту процесу не підхопляться без перезавантаження.
|
|
373
|
+
- **Подвійний обхід дерева**: `collectAutoRuleFacts` і `collectRepoPaths` йдуть по дереву
|
|
374
|
+
окремо — це навмисно: різні задачі (content-факти vs glob-paths) і різні фільтри.
|
|
375
|
+
- **Type C spec (`rules`)** обробляється виключно в `resolveRuleDependencies` — у першому
|
|
376
|
+
проході над `RULE_AUTO_ACTIVATION` ці spec-и пропускаються (`if ('rules' in spec) continue`).
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# `auto-skills.mjs`
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `npm/scripts/auto-skills.mjs` відповідає за **автовизначення (auto-activation) скілів** для файлу конфігурації `.n-cursor.json`. Він сканує директорію `npm/skills/` і для кожного скілу читає його `meta.json`, щоб визначити, чи має скіл активуватися автоматично — завжди або лише за наявності певних виявлених auto-правил (rules).
|
|
6
|
+
|
|
7
|
+
Ключова ідея модуля: **`meta.json` — єдине джерело правди**. У коді немає жорстко прописаної мапи відповідностей між скілами та правилами; усе зчитується з метаданих скіла. Раніше використовувався hardcoded `AUTO_SKILL_ORDER`, тепер він обчислюється динамічно — експорт залишено для зворотної сумісності.
|
|
8
|
+
|
|
9
|
+
Підтримуються три формати поля `auto` у `meta.json`:
|
|
10
|
+
|
|
11
|
+
- `auto: "завжди"` — скіл активується незавжди, незалежно від інших правил. Приклади за коментарями у файлі: `fix`, `lint`, `llm-patch`, `publish-telegram`.
|
|
12
|
+
- `auto: ["rule", …]` — скіл активується, якщо **усі** перелічені правила вже виявлено auto-rules-модулем. Приклади за коментарями: `adr-normalize` залежить від `["adr"]`, `taze` — від `["bun"]`.
|
|
13
|
+
- поле `auto` відсутнє або формат не розпізнано — скіл лише opt-in (тільки через `.n-cursor.json:skills`).
|
|
14
|
+
|
|
15
|
+
Сканування `npm/skills/` виконується **синхронно під час завантаження модуля**: це дає детермінізм результату і узгоджується з sync-API сусіднього модуля `auto-rules.mjs`. Результат кешується на час життя процесу (`SKILL_AUTO_ACTIVATION`).
|
|
16
|
+
|
|
17
|
+
## Експорти / API
|
|
18
|
+
|
|
19
|
+
Модуль експортує:
|
|
20
|
+
|
|
21
|
+
| Символ | Тип | Призначення |
|
|
22
|
+
| ---------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------ |
|
|
23
|
+
| `discoverSkillAutoActivation(skillsDir?)` | function | Сканує директорію зі скілами та повертає мапу `skillId → SkillAutoSpec`. |
|
|
24
|
+
| `AUTO_SKILL_ORDER` | `readonly string[]` (frozen) | Стабільний алфавітний список id скілів, які мають авто-активацію. |
|
|
25
|
+
| `AUTO_SKILL_RULE_DEPENDENCIES` | `Readonly<Record<string, readonly string[]>>` | Лише ті скіли, у яких `auto: [rule, …]` — мапа `skillId → rules[]`. |
|
|
26
|
+
| `detectAutoSkills({ availableSkills, detectedRules, disableSkills? })` | function | Обчислює фінальний список авто-скілів за станом середовища. |
|
|
27
|
+
|
|
28
|
+
Тип `SkillAutoSpec` (визначається JSDoc-typedef):
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
/** @typedef {{ always: true } | { rules: readonly string[] }} SkillAutoSpec */
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Тобто кожен скіл, що пройшов парсинг, або має `always: true`, або несе масив `rules`.
|
|
35
|
+
|
|
36
|
+
## Функції
|
|
37
|
+
|
|
38
|
+
### `discoverSkillAutoActivation(skillsDir = SKILLS_DIR)`
|
|
39
|
+
|
|
40
|
+
**Сигнатура:**
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
function discoverSkillAutoActivation(skillsDir?: string): Record<string, SkillAutoSpec>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Параметри:**
|
|
47
|
+
|
|
48
|
+
- `skillsDir` _(string, optional)_ — шлях до директорії зі скілами. За замовчуванням — `SKILLS_DIR = join(PACKAGE_ROOT, 'skills')`, де `PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))` (тобто корінь npm-пакету). Параметр існує для override у тестах.
|
|
49
|
+
|
|
50
|
+
**Повертає:** `Record<string, SkillAutoSpec>` — мапа `skillId (== ім'я піддиректорії) → spec`.
|
|
51
|
+
|
|
52
|
+
**Поведінка:**
|
|
53
|
+
|
|
54
|
+
1. Якщо `skillsDir` не існує — повертається порожній обʼєкт `{}`.
|
|
55
|
+
2. Інакше викликається `readdirSync(skillsDir, { withFileTypes: true })`.
|
|
56
|
+
3. Для кожного запису:
|
|
57
|
+
- пропускаються файли (не директорії) та все, що починається з `.` (наприклад, приховані).
|
|
58
|
+
- читається `meta.json` через `readSkillMetaRaw(...)`; якщо raw-обʼєкт відсутній (`null`/`undefined`) — скіл пропускається.
|
|
59
|
+
- поле `raw.auto` пропускається через `parseSkillAutoSpec(...)`; якщо розпізнано — записується в `out[entry.name]`.
|
|
60
|
+
4. Скіли без розпізнаного `auto` **не потрапляють у результат** — вони можуть бути ввімкнені лише вручну через `.n-cursor.json:skills`.
|
|
61
|
+
|
|
62
|
+
**Side effects:** sync I/O — `existsSync`, `readdirSync`, читання `meta.json` усередині `readSkillMetaRaw`. Інших побічних ефектів немає.
|
|
63
|
+
|
|
64
|
+
### `detectAutoSkills({ availableSkills, detectedRules, disableSkills })`
|
|
65
|
+
|
|
66
|
+
**Сигнатура:**
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
function detectAutoSkills(params: {
|
|
70
|
+
availableSkills: string[],
|
|
71
|
+
detectedRules: string[],
|
|
72
|
+
disableSkills?: string[],
|
|
73
|
+
}): { skills: string[] }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Параметри:**
|
|
77
|
+
|
|
78
|
+
- `availableSkills` _(string[])_ — перелік id скілів, доступних у поточній збірці пакету (без префіксу `n-`). Будь-який скіл, який є в `SKILL_AUTO_ACTIVATION`, але відсутній у `availableSkills`, **не активується**.
|
|
79
|
+
- `detectedRules` _(string[])_ — id правил, що їх виявив auto-rules-крок; використовується як множина "виявлених" залежностей для специфікацій `{ rules: [...] }`.
|
|
80
|
+
- `disableSkills` _(string[], optional)_ — список id скілів із `.n-cursor.json` із прапором `disable-skills`. За замовчуванням — заморожений пустий масив `DEFAULT_DISABLED_LIST`.
|
|
81
|
+
|
|
82
|
+
**Повертає:** `{ skills: string[] }` — список id активованих скілів у **стабільному алфавітному порядку** (через `AUTO_SKILL_ORDER.filter(...)`).
|
|
83
|
+
|
|
84
|
+
**Алгоритм:**
|
|
85
|
+
|
|
86
|
+
1. Нормалізує `availableSkills` у `Set` (lowercase, trim).
|
|
87
|
+
2. Будує `Set` із `disableSkills` та `detectedRules`.
|
|
88
|
+
3. Для кожної пари `[skillId, spec]` у `SKILL_AUTO_ACTIVATION`:
|
|
89
|
+
- якщо скіл не входить у `normalizedSkills` або входить у `disableSkillsSet` — пропуск.
|
|
90
|
+
- якщо `spec` має `always: true` **або** усі `spec.rules` присутні в `detectedRulesSet` — скіл додається до результуючого `detected`.
|
|
91
|
+
4. Фінальний список будується через `AUTO_SKILL_ORDER.filter(id => detected.has(id))`, що гарантує детермінований алфавітний порядок та фільтрацію тільки тих id, які реально мають spec.
|
|
92
|
+
|
|
93
|
+
**Side effects:** немає — функція чиста. Використовує тільки заздалегідь обчислений `SKILL_AUTO_ACTIVATION` і `AUTO_SKILL_ORDER`.
|
|
94
|
+
|
|
95
|
+
## Внутрішні константи модуля
|
|
96
|
+
|
|
97
|
+
- `PACKAGE_ROOT` — корінь npm-пакету: `dirname(dirname(fileURLToPath(import.meta.url)))`. Тобто два рівні вгору від `npm/scripts/auto-skills.mjs` → `npm/`.
|
|
98
|
+
- `SKILLS_DIR` — `join(PACKAGE_ROOT, 'skills')`, тобто `npm/skills/`.
|
|
99
|
+
- `SKILL_AUTO_ACTIVATION` — результат `discoverSkillAutoActivation()` під час імпорту модуля; кеш на час процесу.
|
|
100
|
+
- `AUTO_SKILL_ORDER` — `Object.freeze(Object.keys(SKILL_AUTO_ACTIVATION).toSorted(localeCompare))`. Заморожений алфавітний список усіх скілів з auto-spec.
|
|
101
|
+
- `AUTO_SKILL_RULE_DEPENDENCIES` — `Object.freeze(Object.fromEntries(...))`: похідна view, де лишаються тільки записи зі spec-формою `{ rules }`. Призначена для зворотної сумісності й для автодоку.
|
|
102
|
+
- `DEFAULT_DISABLED_LIST` — `Object.freeze([])`, дефолтне значення для параметра `disableSkills`.
|
|
103
|
+
|
|
104
|
+
## Залежності
|
|
105
|
+
|
|
106
|
+
**Node.js стандартні модулі:**
|
|
107
|
+
|
|
108
|
+
- `node:fs` — `existsSync`, `readdirSync` (синхронне сканування директорії).
|
|
109
|
+
- `node:path` — `dirname`, `join`.
|
|
110
|
+
- `node:url` — `fileURLToPath` (для отримання абсолютного шляху модуля з `import.meta.url`).
|
|
111
|
+
|
|
112
|
+
**Локальні модулі:**
|
|
113
|
+
|
|
114
|
+
- `./lib/skill-meta.mjs` — імпортуються:
|
|
115
|
+
- `readSkillMetaRaw(skillPath)` — читає сирий обʼєкт `meta.json` за шляхом до директорії скілу.
|
|
116
|
+
- `parseSkillAutoSpec(raw.auto)` — нормалізує значення `auto` у `SkillAutoSpec` (`{ always: true }`, `{ rules: [...] }` або `null`).
|
|
117
|
+
|
|
118
|
+
**Файлова система (рантайм-залежності):**
|
|
119
|
+
|
|
120
|
+
- Очікується наявність директорії `npm/skills/` із піддиректоріями `<skillId>/meta.json`. Відсутність директорії оброблена коректно (повертається `{}`).
|
|
121
|
+
|
|
122
|
+
## Потік виконання / Використання
|
|
123
|
+
|
|
124
|
+
### Завантаження модуля (init phase)
|
|
125
|
+
|
|
126
|
+
1. `PACKAGE_ROOT` і `SKILLS_DIR` обчислюються на основі `import.meta.url`.
|
|
127
|
+
2. Викликається `discoverSkillAutoActivation()` — це **синхронне** I/O під час імпорту: читається `npm/skills/`, з кожної піддиректорії — `meta.json`. Результат зберігається у `SKILL_AUTO_ACTIVATION`.
|
|
128
|
+
3. `AUTO_SKILL_ORDER` і `AUTO_SKILL_RULE_DEPENDENCIES` похідні від цього кешу — обчислюються одноразово і заморожуються.
|
|
129
|
+
|
|
130
|
+
### Виклик `detectAutoSkills` (runtime)
|
|
131
|
+
|
|
132
|
+
Зазвичай викликається на етапі генерації / валідації `.n-cursor.json`. Послідовність:
|
|
133
|
+
|
|
134
|
+
1. Зовнішній код визначає `availableSkills` (зі стану пакету / маніфесту) та `detectedRules` (через сусідній auto-rules pipeline).
|
|
135
|
+
2. Опціонально передає `disableSkills` із конфігу користувача.
|
|
136
|
+
3. Функція повертає `{ skills: [...] }` — фінальний відсортований список id, готовий до запису у конфіг.
|
|
137
|
+
|
|
138
|
+
### Приклад використання
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { detectAutoSkills, AUTO_SKILL_ORDER, AUTO_SKILL_RULE_DEPENDENCIES } from './auto-skills.mjs'
|
|
142
|
+
|
|
143
|
+
const { skills } = detectAutoSkills({
|
|
144
|
+
availableSkills: ['fix', 'lint', 'adr-normalize', 'taze', 'publish-telegram'],
|
|
145
|
+
detectedRules: ['adr', 'bun'],
|
|
146
|
+
disableSkills: ['publish-telegram']
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// skills буде у стабільному алфавітному порядку:
|
|
150
|
+
// напр. ['adr-normalize', 'fix', 'lint', 'taze']
|
|
151
|
+
// (publish-telegram вимкнено через disableSkills)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Контракти й інваріанти
|
|
155
|
+
|
|
156
|
+
- **Детермінізм:** порядок результату повністю визначається `AUTO_SKILL_ORDER`, який є `localeCompare`-сортованим snapshot-ом на момент імпорту.
|
|
157
|
+
- **Безпека до відсутніх скілів:** скіли, доступні у `SKILL_AUTO_ACTIVATION`, але не присутні у `availableSkills`, ніколи не активуються.
|
|
158
|
+
- **Кеш:** перечитати `npm/skills/` без перезавантаження модуля неможливо — функція `discoverSkillAutoActivation` тут лише для прямого виклику з тестів (передаючи інший `skillsDir`).
|
|
159
|
+
- **Опт-аут:** `disableSkills` має пріоритет над `auto`-активацією.
|
|
160
|
+
|
|
161
|
+
## Rebuild Test
|
|
162
|
+
|
|
163
|
+
Уявімо, що файл загублено. За цим описом його можна відновити так:
|
|
164
|
+
|
|
165
|
+
1. ESM-модуль із Node-імпортами `existsSync, readdirSync` із `node:fs`, `dirname, join` із `node:path`, `fileURLToPath` із `node:url`.
|
|
166
|
+
2. Імпорт `parseSkillAutoSpec, readSkillMetaRaw` із `./lib/skill-meta.mjs`.
|
|
167
|
+
3. Константи `PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))`, `SKILLS_DIR = join(PACKAGE_ROOT, 'skills')`.
|
|
168
|
+
4. Експортована функція `discoverSkillAutoActivation(skillsDir = SKILLS_DIR)`: якщо `!existsSync(skillsDir)` → `{}`; інакше readdirSync з `withFileTypes`, ітерувати, пропускати не-директорії та `name.startsWith('.')`, читати `readSkillMetaRaw(join(skillsDir, entry.name))`, пропускати falsy, парсити `parseSkillAutoSpec(raw.auto)`, додавати в `out[entry.name]` тільки якщо парсер повернув spec.
|
|
169
|
+
5. Константа модуля `SKILL_AUTO_ACTIVATION = discoverSkillAutoActivation()`.
|
|
170
|
+
6. Експорт `AUTO_SKILL_ORDER = Object.freeze(Object.keys(SKILL_AUTO_ACTIVATION).toSorted((a,b) => a.localeCompare(b)))`.
|
|
171
|
+
7. Експорт `AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(Object.fromEntries(Object.entries(SKILL_AUTO_ACTIVATION).filter(([, spec]) => 'rules' in spec).map(([id, spec]) => [id, spec.rules])))`.
|
|
172
|
+
8. Константа `DEFAULT_DISABLED_LIST = Object.freeze([])`.
|
|
173
|
+
9. Експортована функція `detectAutoSkills({ availableSkills, detectedRules, disableSkills = DEFAULT_DISABLED_LIST })`: побудувати `normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))`, `disableSkillsSet`, `detectedRulesSet`; пройти `Object.entries(SKILL_AUTO_ACTIVATION)`, пропустити коли скіл не у `normalizedSkills` або у `disableSkillsSet`, інакше додати до `detected` якщо `'always' in spec` або `spec.rules.every(d => detectedRulesSet.has(d))`; повернути `{ skills: AUTO_SKILL_ORDER.filter(id => detected.has(id)) }`.
|