@nitra/cursor 1.11.3 → 1.11.6

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 (86) hide show
  1. package/CHANGELOG.md +32 -3
  2. package/bin/n-cursor.js +40 -6
  3. package/package.json +2 -1
  4. package/rules/abie/js/applies/check.mjs +4 -4
  5. package/rules/abie/js/env_dns/check.mjs +1 -1
  6. package/rules/abie/js/firebase_hosting/check.mjs +1 -1
  7. package/rules/abie/js/hc_pairing/check.mjs +3 -3
  8. package/rules/abie/js/ua_http_route/check.mjs +3 -3
  9. package/rules/abie/js/ua_node_selector/check.mjs +4 -2
  10. package/rules/abie/policy/base_deployment_preem/target.json +1 -5
  11. package/rules/abie/utils/enabled.mjs +1 -1
  12. package/rules/abie/utils/env-dns.mjs +4 -4
  13. package/rules/abie/utils/http-route.mjs +5 -5
  14. package/rules/abie/utils/k8s-tree.mjs +23 -15
  15. package/rules/abie/utils/kustomization-patches.mjs +20 -20
  16. package/rules/abie/utils/overlay-paths.mjs +8 -8
  17. package/rules/abie/utils/yaml.mjs +4 -4
  18. package/rules/adr/adr.mdc +2 -2
  19. package/rules/adr/js/hooks/check.mjs +5 -5
  20. package/rules/docker/docker.mdc +2 -2
  21. package/rules/docker/js/run.mjs +3 -2
  22. package/rules/docker/policy/package_json/package_json.rego +1 -1
  23. package/rules/ga/js/lint.mjs +3 -26
  24. package/rules/hasura/js/internal_urls/check.mjs +1 -1
  25. package/rules/js-bun-redis/js/imports/check.mjs +5 -1
  26. package/rules/js-run/js/runtime/check.mjs +4 -1
  27. package/rules/k8s/js/run.mjs +3 -2
  28. package/rules/k8s/k8s.mdc +2 -4
  29. package/rules/k8s/policy/base_manifest/target.json +1 -5
  30. package/rules/nginx-default-tpl/js/template/check.mjs +4 -2
  31. package/rules/npm-module/js/package_structure/check.mjs +8 -3
  32. package/rules/npm-module/npm-module.mdc +3 -3
  33. package/rules/rego/js/applies/check.mjs +2 -2
  34. package/rules/rego/js/lint.mjs +4 -1
  35. package/rules/rego/policy/package_json/package_json.rego +5 -3
  36. package/rules/rego/rego.mdc +3 -3
  37. package/rules/style-lint/js/tooling/check.mjs +1 -1
  38. package/rules/style-lint/style-lint.mdc +1 -1
  39. package/rules/tauri/js/tooling/check.mjs +3 -1
  40. package/rules/text/js/formatting/check.mjs +8 -24
  41. package/rules/text/js/lint.mjs +34 -0
  42. package/rules/text/js/run-shellcheck.mjs +2 -2
  43. package/rules/text/js/run-v8r.mjs +2 -2
  44. package/rules/text/text.mdc +5 -5
  45. package/schemas/v8r-catalog.json +6 -0
  46. package/scripts/auto-skills.mjs +3 -7
  47. package/scripts/utils/discover-checkable-rules.mjs +4 -3
  48. package/scripts/utils/resolve-target-files.mjs +1 -1
  49. package/scripts/utils/run-lint-step.mjs +33 -0
  50. package/scripts/utils/run-rule.mjs +5 -3
  51. package/skills/abie-clean/SKILL.md +13 -11
  52. package/skills/adr-normalize/SKILL.md +0 -1
  53. package/skills/fix/SKILL.md +3 -7
  54. package/rules/abie/policy/base_deployment_preem/base_deployment_preem_test.rego +0 -60
  55. package/rules/abie/policy/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +0 -48
  56. package/rules/abie/policy/health_check_policy/health_check_policy_test.rego +0 -99
  57. package/rules/abie/policy/http_route_base/http_route_base_test.rego +0 -64
  58. package/rules/bun/policy/package_json/package_json_test.rego +0 -109
  59. package/rules/docker/policy/lint_docker_yml/lint_docker_yml_test.rego +0 -104
  60. package/rules/docker/policy/package_json/package_json_test.rego +0 -42
  61. package/rules/graphql/policy/vscode_extensions/vscode_extensions_test.rego +0 -34
  62. package/rules/image-avif/policy/package_json/package_json_test.rego +0 -69
  63. package/rules/js-lint/policy/package_json/package_json_test.rego +0 -130
  64. package/rules/js-run/policy/jsconfig/jsconfig_test.rego +0 -88
  65. package/rules/k8s/policy/base_kustomization/base_kustomization_test.rego +0 -73
  66. package/rules/k8s/policy/base_manifest/base_manifest_test.rego +0 -94
  67. package/rules/k8s/policy/gateway/gateway_test.rego +0 -122
  68. package/rules/k8s/policy/hasura_configmap/hasura_configmap_test.rego +0 -49
  69. package/rules/k8s/policy/hasura_httproute/hasura_httproute_test.rego +0 -148
  70. package/rules/k8s/policy/hpa_pdb/hpa_pdb_test.rego +0 -101
  71. package/rules/k8s/policy/kustomization/kustomization_test.rego +0 -128
  72. package/rules/k8s/policy/manifest/manifest_test.rego +0 -309
  73. package/rules/k8s/policy/svc_hl_yaml/svc_hl_yaml_test.rego +0 -42
  74. package/rules/k8s/policy/svc_yaml/svc_yaml_test.rego +0 -41
  75. package/rules/nginx-default-tpl/policy/vscode_extensions/vscode_extensions_test.rego +0 -30
  76. package/rules/nginx-default-tpl/policy/vscode_settings/vscode_settings_test.rego +0 -53
  77. package/rules/npm-module/policy/npm_package_json/npm_package_json_test.rego +0 -81
  78. package/rules/rego/policy/package_json/package_json_test.rego +0 -42
  79. package/rules/rego/policy/vscode_extensions/vscode_extensions_test.rego +0 -34
  80. package/rules/rego/policy/vscode_settings/vscode_settings_test.rego +0 -55
  81. package/rules/style-lint/policy/vscode_extensions/vscode_extensions_test.rego +0 -39
  82. package/rules/style-lint/policy/vscode_settings/vscode_settings_test.rego +0 -49
  83. package/rules/tauri/policy/vscode_extensions/vscode_extensions_test.rego +0 -44
  84. package/rules/text/policy/markdownlint/markdownlint_test.rego +0 -98
  85. package/rules/text/policy/vscode_extensions/vscode_extensions_test.rego +0 -51
  86. package/rules/text/policy/vscode_settings/vscode_settings_test.rego +0 -85
package/rules/adr/adr.mdc CHANGED
@@ -39,7 +39,7 @@ Stop-hook `normalize-decisions.sh` спрацьовує на тому самом
39
39
  LLM повертає масив операцій:
40
40
 
41
41
  | `op` | Семантика | Поля |
42
- |---|---|---|
42
+ | --- | --- | --- |
43
43
  | `delete` | Чернетка тривіальна / повністю покрита іншим clean-ADR-ом. | `file`, `reason` |
44
44
  | `rewrite` | Чернетка стає окремим clean-файлом: frontmatter знімається, ім'я → `<slug>.md`, додаються `**Status: Accepted**` і `**Date:**` з `captured`. | `file`, `slug`, `content` |
45
45
  | `merge-into` | Чернетка повторює тему вже існуючого clean-файлу; дописуємо `## Update YYYY-MM-DD` у кінець `target`. | `file`, `target`, `additions` |
@@ -55,7 +55,7 @@ LLM повертає масив операцій:
55
55
  Інший LLM CLI, який запустить normalize, успадковує `ADR_NORMALIZE_RUNNING=1` — внутрішній Stop-hook вийде відразу. Доступні ENV:
56
56
 
57
57
  | Змінна | Default | Призначення |
58
- |---|---|---|
58
+ | --- | --- | --- |
59
59
  | `ADR_NORMALIZE_THRESHOLD` | `30` | Поріг чернеток для запуску фази. |
60
60
  | `ADR_NORMALIZE_BATCH` | `30` | Максимум чернеток у одному виклику LLM. |
61
61
  | `ADR_NORMALIZE_MIN_INTERVAL_HOURS` | `6` | Мінімум між спробами (навіть якщо поріг). |
@@ -79,7 +79,7 @@ function gitignoreLineCoversHookLog(line, logPath) {
79
79
 
80
80
  /**
81
81
  * Перевіряє наявність і канонічність одного hook-скрипта.
82
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
82
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
83
83
  * @param {string} scriptName базове ім'я скрипта (наприклад `capture-decisions.sh`)
84
84
  * @returns {Promise<void>}
85
85
  */
@@ -108,7 +108,7 @@ async function checkHookScript(reporter, scriptName) {
108
108
  * `.claude/settings.local.json`. Структуру (`hooks.Stop[]` містить групу з
109
109
  * `capture-decisions.sh`; `settings.local.json` не дублює) валідують
110
110
  * `npm/policy/adr/settings_json/` і `npm/policy/adr/settings_local_json/`.
111
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер
111
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер
112
112
  */
113
113
  function checkProjectSettings(reporter) {
114
114
  const { pass, fail } = reporter
@@ -121,7 +121,7 @@ function checkProjectSettings(reporter) {
121
121
 
122
122
  /**
123
123
  * Перевіряє `.gitignore` на ігнорування лог-файлу одного хука.
124
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
124
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
125
125
  * @param {string} logName базове ім'я лог-файлу (наприклад `capture-decisions.log`)
126
126
  * @param {string} gitignoreContent попередньо прочитаний вміст `.gitignore`
127
127
  * @returns {void}
@@ -142,7 +142,7 @@ function checkGitignoreForLog(reporter, logName, gitignoreContent) {
142
142
 
143
143
  /**
144
144
  * Перевіряє `.gitignore` для всіх hook-логів одним проходом.
145
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
145
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
146
146
  * @returns {Promise<void>}
147
147
  */
148
148
  async function checkGitignore(reporter) {
@@ -182,7 +182,7 @@ function isBinaryInPath(name) {
182
182
  /**
183
183
  * Інформативна перевірка: чи доступний бодай один LLM CLI (`claude` або `cursor-agent`).
184
184
  * Якщо жодного немає — це warning (`pass` з підказкою), бо хук просто мовчки no-op'ає.
185
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
185
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
186
186
  * @returns {void}
187
187
  */
188
188
  function checkLlmCliAvailable(reporter) {
@@ -97,7 +97,7 @@ CMD ["./app"]
97
97
 
98
98
  ## lint-docker
99
99
 
100
- CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE...]` у **`hadolint --help`**); обхід репозиторію робить скрипт **`npm/scripts/run-docker.mjs`**.
100
+ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE...]` у **`hadolint --help`**); обхід репозиторію робить CLI **`n-cursor lint-docker`** (реалізація — **`npm/rules/docker/js/run.mjs`**).
101
101
 
102
102
  **Область lint-docker (вужча, ніж `check docker`):** лише файли з іменем **`Dockerfile`** та **`*.Dockerfile`** (суфікс **`.dockerfile`** без урахування регістру, наприклад **`api.Dockerfile`**). Файли **`Dockerfile.prod`**, **`Containerfile`** тощо **не** входять у **`lint-docker`**; їх ловить **`check docker`** (`check-docker.mjs`).
103
103
 
@@ -106,7 +106,7 @@ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE
106
106
  ```json title="package.json"
107
107
  {
108
108
  "scripts": {
109
- "lint-docker": "bun ./npm/scripts/run-docker.mjs"
109
+ "lint-docker": "n-cursor lint-docker"
110
110
  }
111
111
  }
112
112
  ```
@@ -47,9 +47,10 @@ export async function findLintDockerfilePaths(root, ignorePaths = []) {
47
47
 
48
48
  /**
49
49
  * Запуск hadolint по Dockerfile та *.Dockerfile.
50
+ * Експортовано як `runLintDocker` — використовується з `bin/n-cursor.js` як підкоманда `lint-docker`.
50
51
  * @returns {Promise<number>} 0 — OK, 1 — зауваження або помилка
51
52
  */
52
- async function main() {
53
+ export async function runLintDocker() {
53
54
  const reporter = createCheckReporter()
54
55
  const { pass, fail } = reporter
55
56
 
@@ -80,5 +81,5 @@ async function main() {
80
81
  }
81
82
 
82
83
  if (isRunAsCli()) {
83
- process.exitCode = await main()
84
+ process.exitCode = await runLintDocker()
84
85
  }
@@ -19,7 +19,7 @@ package docker.package_json
19
19
 
20
20
  import rego.v1
21
21
 
22
- canonical_lint_docker := "bun ./npm/scripts/run-docker.mjs"
22
+ canonical_lint_docker := "n-cursor lint-docker"
23
23
 
24
24
  lint_docker_template := concat(" ", [
25
25
  "package.json: scripts.lint-docker має бути %q",
@@ -19,11 +19,11 @@
19
19
  *
20
20
  * Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
21
21
  */
22
- import { spawnSync } from 'node:child_process'
23
22
  import { platform } from 'node:process'
24
23
 
25
24
  import { check as checkGa } from './workflows/check.mjs'
26
25
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
26
+ import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
27
27
 
28
28
  /**
29
29
  * Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
@@ -112,29 +112,6 @@ function preflight(dep) {
112
112
  return false
113
113
  }
114
114
 
115
- /**
116
- * Запускає крок lint-ga з відображенням команди користувачу. Stdout/stderr дочірнього процесу
117
- * передається користувачу як є (`stdio: 'inherit'`), щоб виглядало як прямий виклик у shell.
118
- * @param {string} title заголовок для логу (наприклад `actionlint`)
119
- * @param {string} cmd ім'я команди (`bunx`, `uvx`)
120
- * @param {string[]} args аргументи команди
121
- * @returns {number} код виходу дочірнього процесу (0 — OK, інше — помилка)
122
- */
123
- function runStep(title, cmd, args) {
124
- console.log(`\n▶ ${title}: ${cmd} ${args.join(' ')}`)
125
- const resolved = resolveCmd(cmd)
126
- if (!resolved) {
127
- console.error(`❌ ${cmd} не знайдено в PATH (${title}).`)
128
- return 127
129
- }
130
- const r = spawnSync(resolved, args, { stdio: 'inherit', env: process.env })
131
- if (r.error) {
132
- console.error(`❌ Не вдалося запустити ${cmd}: ${r.error.message}`)
133
- return 1
134
- }
135
- return r.status ?? 1
136
- }
137
-
138
115
  /**
139
116
  * Виконує канонічний `lint-ga` з preflight-перевірками і делегує до `check-ga.check()`.
140
117
  *
@@ -160,10 +137,10 @@ export async function runLintGaCli() {
160
137
  }
161
138
  if (!preflightOk) return 1
162
139
 
163
- const actionlintCode = runStep('actionlint', 'bunx', ['github-actionlint'])
140
+ const actionlintCode = runLintStep('actionlint', 'bunx', ['github-actionlint'])
164
141
  if (actionlintCode !== 0) return actionlintCode
165
142
 
166
- const zizmorCode = runStep('zizmor', 'uvx', ['zizmor', '--offline', '--collect=workflows', '.'])
143
+ const zizmorCode = runLintStep('zizmor', 'uvx', ['zizmor', '--offline', '--collect=workflows', '.'])
167
144
  if (zizmorCode !== 0) return zizmorCode
168
145
 
169
146
  console.log('\n▶ check-ga (rego-полісі npm/policy/ga/ + JS cross-file перевірки)')
@@ -143,7 +143,7 @@ async function checkEnvFile(relPath, expected, reporter) {
143
143
  const value = m[1].trim()
144
144
  const parsed = parseInternalHasuraEndpoint(value)
145
145
  if (!parsed.ok) {
146
- const example = "https://<service>.<namespace>.svc.<cluster>.internal:<port>"
146
+ const example = 'https://<service>.<namespace>.svc.<cluster>.internal:<port>'
147
147
  fail(
148
148
  `${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
149
149
  )
@@ -16,7 +16,11 @@ import { join, relative } from 'node:path'
16
16
 
17
17
  import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
18
18
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
19
- import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from '../../../../scripts/utils/redis-imports.mjs'
19
+ import {
20
+ findRedisImportsInText,
21
+ isRedisScanSourceFile,
22
+ shouldSkipFileForRedisScan
23
+ } from '../../../../scripts/utils/redis-imports.mjs'
20
24
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
21
25
 
22
26
  /**
@@ -49,7 +49,10 @@ import {
49
49
  resolveConnDirFromPackageJson
50
50
  } from '../../../../scripts/utils/conn-imports-scan.mjs'
51
51
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
52
- import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '../../../../scripts/utils/promise-settimeout-scan.mjs'
52
+ import {
53
+ findPromiseSetTimeoutInText,
54
+ isPromiseSetTimeoutScanSourceFile
55
+ } from '../../../../scripts/utils/promise-settimeout-scan.mjs'
53
56
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
54
57
  import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
55
58
 
@@ -146,9 +146,10 @@ function runKubescape(dirs) {
146
146
 
147
147
  /**
148
148
  * Головна точка входу: kubeconform + kubescape для усіх знайдених дерев `k8s`.
149
+ * Експортовано як `runLintK8s` — використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
149
150
  * @returns {Promise<number>} код виходу для `process.exitCode` (0 — успіх або пропуск)
150
151
  */
151
- async function main() {
152
+ export async function runLintK8s() {
152
153
  const root = process.cwd()
153
154
  const ignorePaths = await loadCursorIgnorePaths(root)
154
155
  const dirs = await findK8sRoots(root, ignorePaths)
@@ -169,5 +170,5 @@ async function main() {
169
170
  }
170
171
 
171
172
  if (isRunAsCli()) {
172
- process.exitCode = await main()
173
+ process.exitCode = await runLintK8s()
173
174
  }
package/rules/k8s/k8s.mdc CHANGED
@@ -38,20 +38,18 @@ alwaysApply: false
38
38
 
39
39
  **kubescape:** типово **`kubescape scan <каталог-k8s>`**; поріг серйозності підлаштуй під проєкт (наприклад **`--severity-threshold high`**). Перший запуск може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
40
40
 
41
- У репозиторії пакета **`@nitra/cursor`** скрипт **`lint-k8s`** делегує обхід дерев і виклики **`npm/scripts/run-k8s.mjs`**. У інших проєктах можна скопіювати цей скрипт або зібрати еквівалентні команди в **`package.json`**.
41
+ У репозиторії пакета **`@nitra/cursor`** скрипт **`lint-k8s`** делегує до CLI **`n-cursor lint-k8s`** (реалізація — **`npm/rules/k8s/js/run.mjs`**). У інших проєктах достатньо встановити **`@nitra/cursor`** у `devDependencies` бінарка **`n-cursor`** буде у **`node_modules/.bin/`**.
42
42
 
43
43
  ```json title="package.json"
44
44
  {
45
45
  "scripts": {
46
- "lint-k8s": "bun ./npm/scripts/run-k8s.mjs"
46
+ "lint-k8s": "n-cursor lint-k8s"
47
47
  }
48
48
  }
49
49
  ```
50
50
 
51
51
  Якщо правило **`k8s`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json` **мають** бути скрипт **`lint-k8s`** і виклик **`bun run lint-k8s`** у агрегованому **`lint`** (див. **`bun.mdc`**). Це перевіряє **`npx @nitra/cursor check bun`**.
52
52
 
53
- Шлях до скрипта підстав свій (`./scripts/…` після копіювання, `node_modules/@nitra/cursor/scripts/…` якщо пакет у залежностях).
54
-
55
53
  Додай workflow **`.github/workflows/lint-k8s.yml`** (гілки **`dev`** і **`main`**, лише **`.yml`**, узгоджено з **`ga.mdc`**). **Не** дублюй **`setup-node`**, **`oven-sh/setup-bun`**, **`actions/cache`** і **`bun install`** у job — після **`checkout`** використовуй локальний composite **`setup-bun-deps`** (шлях **`./.github/actions/setup-bun-deps`** або **`./npm/github-actions/setup-bun-deps`**, як у репозиторії). Встановлення **kubeconform** / **kubescape** лишаються окремими кроками.
56
54
 
57
55
  ```yaml title=".github/workflows/lint-k8s.yml"
@@ -1,10 +1,6 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
3
  "files": {
4
- "walkGlob": [
5
- "**/k8s/**/base/**/*.yaml",
6
- "**/k8s/**/base/**/*.yml",
7
- "!**/k8s/**/base/**/kustomization.yaml"
8
- ]
4
+ "walkGlob": ["**/k8s/**/base/**/*.yaml", "**/k8s/**/base/**/*.yml", "!**/k8s/**/base/**/kustomization.yaml"]
9
5
  }
10
6
  }
@@ -34,7 +34,9 @@ const GZIP_CMD_RE = /\bgzip\b/u
34
34
  const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
35
35
 
36
36
  /**
37
- * Збирає абсолютні шляхи до **default.conf.template** у репозиторії; шлях `tests/fixtures` не обходиться як проєктний шаблон.
37
+ * Збирає абсолютні шляхи до **default.conf.template** у репозиторії; будь-який сегмент
38
+ * `fixtures/` у шляху виключається — це тестові артефакти (як `tests/fixtures/` так і
39
+ * co-located `rules/<rule>/js/<concern>/fixtures/`).
38
40
  * @param {string} root корінь cwd
39
41
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
40
42
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до шаблонів
@@ -47,7 +49,7 @@ export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
47
49
  p => {
48
50
  if (basename(p) !== 'default.conf.template') return
49
51
  const rel = relative(root, p).replaceAll('\\', '/')
50
- if (rel.includes('tests/fixtures/')) return
52
+ if (rel.split('/').includes('fixtures')) return
51
53
  out.push(p)
52
54
  },
53
55
  ignorePaths
@@ -32,7 +32,12 @@ 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'
35
+ import {
36
+ dynamicImportModule,
37
+ langFromPath,
38
+ requireCallModule,
39
+ walkAstWithAncestors
40
+ } from '../../../../scripts/utils/ast-scan-utils.mjs'
36
41
  import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
37
42
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
38
43
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
@@ -491,8 +496,8 @@ async function checkNoTestsInPublishedFiles(pass, fail) {
491
496
  }
492
497
  for (const v of violations) {
493
498
  fail(
494
- `npm/${v.file}: ${v.reason} — винеси за межі шляхів з "files" або додай негативний glob ` +
495
- '(наприклад "!**/*_test.rego") у npm/package.json (npm-module.mdc)'
499
+ `npm/${v.file}: ${v.reason} — додай у "files" у npm/package.json негативний glob, ` +
500
+ 'що виключає цей файл з tarball (наприклад "!**/*.test.mjs", "!**/fixtures/**", "!**/*_test.rego") (npm-module.mdc)'
496
501
  )
497
502
  }
498
503
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Оформлення репозиторію для npm модуля
3
3
  alwaysApply: true
4
- version: '1.11'
4
+ version: '1.12'
5
5
  ---
6
6
 
7
7
  Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
@@ -11,13 +11,13 @@ Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.g
11
11
  Мета — **максимально компактний** опублікований пакет: у npm потрапляє тільки те, що потрібно під час `require`/`import` користувачем.
12
12
 
13
13
  - **`"files"` обовʼязковий** у `npm/package.json` як **whitelist** того, що публікується (без `"files"` npm пакує майже все — це антипатерн для цього правила).
14
- - **Тести й фікстури — поза будь-яким шляхом з `"files"`.** Канонічне місце для всього тестового `npm/tests/` (його **не** додаємо до `"files"`). Це стосується **і** фреймворк-тестів (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, …), **і** test-style каталогів (`tests/`, `__tests__/`, `fixtures/`, `__fixtures__/`, `spec/`), **і** файлів за патернами `*.test.*` / `*.spec.*`. **Виняток Rego (`*_test.rego`):** за конвенцією conftest юніт-тест лежить поруч з полісі у тому самому `package`, тож rego-тести дозволені всередині опублікованого `policy/`-каталогу.
14
+ - **Тести й фікстури не публікуються через негативні glob-патерни у `"files"`.** Тести можна зазвичай зручніше) тримати **поруч з кодом** усередині шляхів, перелічених у `"files"` (наприклад `*.test.mjs` поруч з модулем чи `fixtures/` під `rules/<id>/js/`). Щоб вони не потрапляли у tarball, `"files"` **обовʼязково має містити негативні glob-патерни**, що їх виключають. Покрий усі форми тестового: фреймворк-тести (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, …), test-style каталоги (`tests/`, `__tests__/`, `fixtures/`, `__fixtures__/`, `spec/`, `test/`), файли за патернами `*.test.*` / `*.spec.*`. Орієнтовний набір: `"!**/*.test.*"`, `"!**/*.spec.*"`, `"!**/test-helpers.*"`, `"!**/fixtures/**"`, `"!**/__tests__/**"`. **Rego (`*_test.rego`):** за конвенцією conftest юніт-тест лежить поруч з полісі у тому самому `package` `*_test.rego` усередині опублікованого `policy/` дозволені; якщо потрібна максимальна компактність, додай `"!**/*_test.rego"` явно (як у самому `@nitra/cursor`).
15
15
  - **Лише runtime-залежності у `npm/package.json`.** `devDependencies` тримай у **кореневому** `package.json` монорепо — тоді `npm install @nitra/<pkg>` не тягне інструментарій, потрібний лише для розробки самого пакета.
16
16
 
17
17
  Деталі алгоритму перевірки:
18
18
 
19
19
  - Пер-документні правила для `npm/package.json` (whitelist `files`, заборона `devDependencies`, форма `types`) — у rego-пакеті `npm_module.npm_package_json` (`npm/policy/npm_module/npm_package_json/`).
20
- - Скан опублікованого простору файлів на тест-патерни (walking + AST JS/TS) — у `check-npm-module.mjs::checkNoTestsInPublishedFiles` (FS / AST не лягають у rego).
20
+ - Walk шляхів з `"files"` з застосуванням негативних patterns і скан залишку на тест-патерни (walking + AST JS/TS) — у `check.mjs::checkNoTestsInPublishedFiles` (FS / AST не лягають у rego). Якщо після застосування негативних patterns у tarball лишається test-style файл — `check` падає з вказівкою, який саме негативний glob треба додати у `"files"`.
21
21
 
22
22
  ## TypeScript declaration (`npm/types`)
23
23
 
@@ -45,9 +45,9 @@ export async function applies() {
45
45
 
46
46
  /**
47
47
  * Друкує короткий context-pass — самі полісі прогонить CLI через `policy/<name>/target.json`.
48
- * @returns {Promise<number>} 0 — все ок (фактичні порушення повертають policy-концерни)
48
+ * @returns {number} 0 — все ок (фактичні порушення повертають policy-концерни)
49
49
  */
50
- export async function check() {
50
+ export function check() {
51
51
  const reporter = createCheckReporter()
52
52
  reporter.pass('Знайдено *.rego у дереві — перевіряємо канонічні конфіги rego.mdc')
53
53
  return reporter.getExitCode()
@@ -26,6 +26,7 @@ import { spawnSync } from 'node:child_process'
26
26
  import { existsSync } from 'node:fs'
27
27
  import { resolve } from 'node:path'
28
28
 
29
+ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
29
30
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
30
31
 
31
32
  /** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
@@ -128,4 +129,6 @@ export function runLintRego(cwd = process.cwd()) {
128
129
  return runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)
129
130
  }
130
131
 
131
- process.exitCode = runLintRego()
132
+ if (isRunAsCli()) {
133
+ process.exitCode = runLintRego()
134
+ }
@@ -3,14 +3,16 @@
3
3
  # Викликається з `check-rego.mjs` через `runConftestBatch` лише ПІСЛЯ виявлення
4
4
  # `.rego` файлів у дереві. Глобально у `lint-conftest` НЕ реєструється.
5
5
  #
6
- # Canonical (rego.mdc): scripts.lint-rego має бути "bun ./npm/scripts/lint-rego.mjs".
6
+ # Canonical (rego.mdc): scripts.lint-rego має бути "n-cursor lint-rego" — CLI пакета `@nitra/cursor`
7
+ # (бінарка з `node_modules/.bin/`) робить preflight `opa`/`regal` і прогонить
8
+ # `opa check --strict` → `regal lint` → опц. `conftest verify` (`@nitra/cursor lint-rego`).
7
9
  #
8
10
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
9
11
  package rego.package_json
10
12
 
11
13
  import rego.v1
12
14
 
13
- canonical_lint_rego := "bun ./npm/scripts/lint-rego.mjs"
15
+ canonical_lint_rego := "n-cursor lint-rego"
14
16
 
15
17
  lint_rego_template := concat(" ", [
16
18
  "package.json: scripts.lint-rego має бути %q",
@@ -22,7 +24,7 @@ deny contains msg if {
22
24
  not "lint-rego" in object.keys(scripts)
23
25
  msg := concat(" ", [
24
26
  "package.json: відсутній scripts.lint-rego — додай",
25
- "\"lint-rego\": \"bun ./npm/scripts/lint-rego.mjs\" (rego.mdc)",
27
+ "\"lint-rego\": \"n-cursor lint-rego\" (rego.mdc)",
26
28
  ])
27
29
  }
28
30
 
@@ -36,13 +36,13 @@ alwaysApply: false
36
36
  bun run lint-rego
37
37
  ```
38
38
 
39
- Скрипт делегує до `bun ./npm/scripts/lint-rego.mjs`. Послідовність:
39
+ Скрипт делегує до CLI `n-cursor lint-rego` (бінарка з `node_modules/.bin/` пакету `@nitra/cursor`). Послідовність:
40
40
 
41
41
  1. **preflight** — наявність `opa` і `regal` у `PATH`; якщо хоча б одного нема — exit 1 з підказкою встановлення;
42
42
  2. `opa check --strict <targets>` — компіляція з типами та `--strict` (мертвий код, неоднозначні правила, незадекларовані змінні);
43
43
  3. `regal lint <targets>` — статичний лінтер Rego ([Styra Regal](https://docs.styra.com/regal)): ловить v0-синтаксис, неявні set-rules і відхилення від `rego.v1`, плюс bugs/idiomatic/style-правила.
44
44
 
45
- Цілі — `npm/policy/` (де живуть Rego-полісі пакета). Інші *.rego поза деревом додай у `LINT_TARGETS` у `lint-rego.mjs`.
45
+ Цілі — `npm/policy/` (де живуть Rego-полісі пакета). Інші *.rego поза деревом додай у `LINT_TARGETS` у `npm/rules/rego/js/lint.mjs`.
46
46
 
47
47
  ### Встановлення інструментів
48
48
 
@@ -58,7 +58,7 @@ bun run lint-rego
58
58
  ```json title="package.json"
59
59
  {
60
60
  "scripts": {
61
- "lint-rego": "bun ./npm/scripts/lint-rego.mjs"
61
+ "lint-rego": "n-cursor lint-rego"
62
62
  }
63
63
  }
64
64
  ```
@@ -25,7 +25,7 @@ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mj
25
25
  * Альтернатива полю `stylelint` у `package.json` — зовнішній файл конфігу. Якщо
26
26
  * поля немає і файлу немає, фейлимося; якщо є хоч щось — пропускаємо. Поле
27
27
  * `stylelint.extends == "@nitra/stylelint-config"` сам формат — у Rego.
28
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер
28
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер
29
29
  */
30
30
  async function checkStylelintConfigPresence(reporter) {
31
31
  const { pass, fail } = reporter
@@ -9,7 +9,7 @@ alwaysApply: false
9
9
 
10
10
  - **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
11
11
  - **Форматування** узгоджуй з **`n-text.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
12
- - **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
12
+ - **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний `bun run lint` (навичка **`/n-lint`**).
13
13
  - **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
14
14
 
15
15
  **VSCode:** у **`.vscode/extensions.json`** рекомендуй **`stylelint.vscode-stylelint`**. У **`.vscode/settings.json`** вимкни вбудовану валідацію CSS/SCSS/Less і увімкни явні code actions:
@@ -70,7 +70,9 @@ export async function check() {
70
70
 
71
71
  const extPath = '.vscode/extensions.json'
72
72
  if (!existsSync(extPath)) {
73
- fail(`${extPath} не існує — створи з recommendations "tauri-apps.tauri-vscode" і "rust-lang.rust-analyzer" (tauri.mdc)`)
73
+ fail(
74
+ `${extPath} не існує — створи з recommendations "tauri-apps.tauri-vscode" і "rust-lang.rust-analyzer" (tauri.mdc)`
75
+ )
74
76
  return reporter.getExitCode()
75
77
  }
76
78
  const violations = runConftestBatch({
@@ -147,36 +147,20 @@ async function checkPackageJsonText(passFn, failFn) {
147
147
  }
148
148
 
149
149
  /**
150
- * Перевіряє скрипт lint-text на коректність v8r-виклику.
151
- * @param {unknown} lintText параметр lintText
150
+ * Перевіряє скрипт lint-text: канонічний `n-cursor lint-text` (CLI пакета `@nitra/cursor` робить
151
+ * `cspell` `runShellcheckText()` `bunx markdownlint-cli2 --fix` → `runV8rWithGlobs()`).
152
+ * Дозволено whitespace навколо команди.
153
+ * @param {unknown} lintText значення `scripts.lint-text` з package.json
152
154
  * @param {(msg: string) => void} passFn callback при успішній перевірці
153
155
  * @param {(msg: string) => void} failFn callback при помилці
154
156
  */
155
157
  function checkLintTextScript(lintText, passFn, failFn) {
156
- const lt = typeof lintText === 'string' ? lintText : ''
157
- const v8rCalls = (lt.match(/bunx v8r/g) || []).length
158
- const quietCalls = (lt.match(/run-v8r?\.mjs/g) || []).length
159
- const eq98Hints = (lt.match(/eq 98/g) || []).length
160
- const legacyV8r = v8rCalls >= 4 && eq98Hints >= 4
161
- const quietBundled = quietCalls === 1
162
- const quietLegacy4x = quietCalls >= 4
163
- const v8rTextOk = legacyV8r || quietBundled || quietLegacy4x
164
- const globsRequired = legacyV8r || quietLegacy4x
165
- const globsOk =
166
- lt.includes('**/*.json') && lt.includes('**/*.yml') && lt.includes('**/*.yaml') && lt.includes('**/*.toml')
167
- const ok =
168
- lt &&
169
- lt.includes('cspell') &&
170
- lt.includes('run-shellcheck-text.mjs') &&
171
- lt.includes('bunx markdownlint-cli2') &&
172
- lt.includes('**/*.mdc') &&
173
- v8rTextOk &&
174
- (!globsRequired || globsOk)
175
- if (ok) {
176
- passFn('package.json: lint-text — shellcheck (run-shellcheck-text.mjs), v8r: run-v8r.mjs або чотири bunx v8r')
158
+ const lt = typeof lintText === 'string' ? lintText.trim() : ''
159
+ if (lt === 'n-cursor lint-text') {
160
+ passFn('lint-text делегує CLI n-cursor lint-text (cspell + shellcheck + markdownlint + v8r)')
177
161
  } else {
178
162
  failFn(
179
- 'package.json: lint-text додай bun ./…/run-shellcheck-text.mjs; v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) (див. n-text.mdc)'
163
+ 'package.json: lint-text має бути "n-cursor lint-text" CLI пакета @nitra/cursor виконує cspell shellcheck markdownlint-cli2 v8r (text.mdc)'
180
164
  )
181
165
  }
182
166
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CLI-обгортка над канонічним `lint-text` (text.mdc): послідовно
3
+ * 1) `cspell .` — перевірка правопису з `@nitra/cspell-dict`;
4
+ * 2) `runShellcheckText()` — авто-фікс і фінальна перевірка `*.sh` через `shellcheck`;
5
+ * 3) `bunx markdownlint-cli2 --fix "**\/*.md" "**\/*.mdc"` — авто-фікс Markdown;
6
+ * 4) `runV8rWithGlobs()` — schema-валідація json/json5/yaml/yml/toml через v8r з каталогом `@nitra/cursor`.
7
+ *
8
+ * Перший ненульовий код з ланцюжка повертається як код виходу; наступні кроки не запускаються.
9
+ * Експортовано як `runLintTextCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-text`.
10
+ */
11
+ import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
12
+ import { runShellcheckText } from './run-shellcheck.mjs'
13
+ import { runV8rWithGlobs } from './run-v8r.mjs'
14
+
15
+ /**
16
+ * Виконує канонічний `lint-text`: cspell → run-shellcheck → markdownlint-cli2 → run-v8r.
17
+ * Першу помилку повертаємо як код виходу; наступні кроки не запускаються.
18
+ * Усі кроки синхронні (`spawnSync` + sync-ентрі з пакета), тому функція не async.
19
+ * @returns {number} 0 — все OK, інакше — код першого кроку, що впав
20
+ */
21
+ export function runLintTextCli() {
22
+ const cspellCode = runLintStep('cspell', 'npx', ['cspell', '.'])
23
+ if (cspellCode !== 0) return cspellCode
24
+
25
+ console.log('\n▶ shellcheck (авто-фікс + фінальна перевірка *.sh)')
26
+ const shellcheckCode = runShellcheckText()
27
+ if (shellcheckCode !== 0) return shellcheckCode
28
+
29
+ const markdownlintCode = runLintStep('markdownlint', 'bunx', ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc'])
30
+ if (markdownlintCode !== 0) return markdownlintCode
31
+
32
+ console.log('\n▶ v8r (schema-валідація json/json5/yaml/yml/toml)')
33
+ return runV8rWithGlobs()
34
+ }
@@ -82,7 +82,7 @@ export function listShellScriptPaths(cwd) {
82
82
  return []
83
83
  }
84
84
  const files = ls.stdout.split('\0').filter(Boolean)
85
- return new Set(files).toSorted()
85
+ return [...new Set(files)].toSorted()
86
86
  }
87
87
  }
88
88
 
@@ -90,7 +90,7 @@ export function listShellScriptPaths(cwd) {
90
90
  cwd,
91
91
  exclude: p => p.includes('node_modules') || p.startsWith(`node_modules/`) || p.split('/').includes('node_modules')
92
92
  })
93
- return new Set(fromGlob.map(p => p.replaceAll('\\', '/'))).toSorted()
93
+ return [...new Set(fromGlob.map(p => p.replaceAll('\\', '/')))].toSorted()
94
94
  }
95
95
 
96
96
  /**
@@ -24,8 +24,8 @@ import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
24
24
  /** Типові glob-и для форматів, які обробляє v8r (див. опис CLI v8r). */
25
25
  export const DEFAULT_V8R_GLOBS = ['**/*.json', '**/*.json5', '**/*.yml', '**/*.yaml', '**/*.toml']
26
26
 
27
- /** Абсолютний шлях до `schemas/v8r-catalog.json` поруч з цим скриптом у пакеті `@nitra/cursor`. */
28
- export const V8R_CATALOG_PATH = join(dirname(fileURLToPath(import.meta.url)), '../schemas/v8r-catalog.json')
27
+ /** Абсолютний шлях до `schemas/v8r-catalog.json` у корені пакета `@nitra/cursor` (`npm/schemas/`). */
28
+ export const V8R_CATALOG_PATH = join(dirname(fileURLToPath(import.meta.url)), '../../../schemas/v8r-catalog.json')
29
29
 
30
30
  /**
31
31
  * Повертає шлях до каталогу схем v8r для пакета (для тестів і діагностики).