@nitra/cursor 1.13.75 → 1.13.82

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 (51) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/package.json +1 -1
  3. package/rules/abie/abie.mdc +11 -3
  4. package/rules/abie/policy/base_deployment_preem/base_deployment_preem.rego +9 -11
  5. package/rules/abie/policy/health_check_policy/health_check_policy.rego +9 -10
  6. package/rules/abie/policy/http_route_base/http_route_base.rego +7 -8
  7. package/rules/changelog/fix/consistency/check.mjs +16 -16
  8. package/{scripts/utils → rules/changelog/fix/consistency}/package-manifest.mjs +1 -1
  9. package/rules/docker/docker.mdc +3 -3
  10. package/rules/docker/fix/lint/check.mjs +3 -3
  11. package/{scripts/utils → rules/docker/fix/lint}/docker-hadolint.mjs +3 -3
  12. package/rules/docker/lint/lint.mjs +2 -2
  13. package/rules/ga/fix/workflows/check.mjs +5 -1
  14. package/rules/ga/ga.mdc +8 -1
  15. package/rules/ga/lint/lint.mjs +4 -1
  16. package/rules/ga/policy/workflow_common/template/uses-min-versions.snippet.json +4 -0
  17. package/rules/ga/policy/workflow_common/workflow_common.rego +89 -0
  18. package/rules/graphql/fix/tooling/check.mjs +1 -1
  19. package/{scripts/utils → rules/graphql/fix/tooling}/graphql-gql-scan.mjs +47 -12
  20. package/rules/hasura/fix/internal_urls/check.mjs +1 -1
  21. package/{scripts/utils → rules/js-bun-db/fix/safety}/bun-sql-scan.mjs +1 -1
  22. package/rules/js-bun-db/fix/safety/check.mjs +1 -1
  23. package/rules/js-lint/fix/tooling/check.mjs +3 -15
  24. package/{scripts/utils → rules/js-lint/fix/tooling}/rebuild-oxlint-canonical.mjs +1 -1
  25. package/rules/js-lint/js-lint.mdc +2 -2
  26. package/rules/js-mssql/fix/deps/check.mjs +1 -1
  27. package/{scripts/utils → rules/js-mssql/fix/deps}/mssql-pool-scan.mjs +1 -1
  28. package/{scripts/utils → rules/js-run/fix/runtime}/bunyan-imports.mjs +1 -1
  29. package/{scripts/utils → rules/js-run/fix/runtime}/check-env-scan.mjs +1 -1
  30. package/rules/js-run/fix/runtime/check.mjs +5 -5
  31. package/{scripts/utils → rules/js-run/fix/runtime}/conn-file-rules.mjs +1 -1
  32. package/{scripts/utils → rules/js-run/fix/runtime}/conn-imports-scan.mjs +1 -1
  33. package/{scripts/utils → rules/js-run/fix/runtime}/promise-settimeout-scan.mjs +1 -1
  34. package/rules/k8s/k8s.mdc +1 -1
  35. package/rules/npm-module/fix/package_structure/check.mjs +11 -1
  36. package/rules/test/auto.md +1 -0
  37. package/rules/test/fix/location/check.mjs +77 -0
  38. package/rules/test/test.mdc +60 -0
  39. package/rules/vue/fix/packages/check.mjs +2 -2
  40. package/rules/vue/vue.mdc +1 -1
  41. package/scripts/auto-rules.mjs +3 -3
  42. package/scripts/utils/ast-scan-utils.mjs +3 -2
  43. package/scripts/utils/with-lock.mjs +120 -0
  44. package/scripts/utils/workspaces.mjs +1 -1
  45. package/scripts/utils/worktree-fingerprint.mjs +30 -0
  46. /package/{scripts/utils → rules/docker/fix/lint}/docker-mirror.mjs +0 -0
  47. /package/{scripts/utils → rules/js-lint/fix/tooling}/knip-canonical.json +0 -0
  48. /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-canonical-skeleton.json +0 -0
  49. /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-canonical.json +0 -0
  50. /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-rules.tsv +0 -0
  51. /package/{scripts/utils → rules/vue/fix/packages}/vue-forbidden-imports.mjs +0 -0
@@ -55,6 +55,11 @@ setup_bun_no_checkout_template := concat(" ", [
55
55
  "інакше runner не знайде action.yml (ga.mdc)",
56
56
  ])
57
57
 
58
+ min_uses_version_template := concat(" ", [
59
+ "jobs.%s.steps[%d]: %s має бути >= v%s (зараз %q) —",
60
+ "онови ref у uses: (ga.mdc)",
61
+ ])
62
+
58
63
  forbidden_run_command_template := concat(" ", [
59
64
  "jobs.%s.steps[%d]: `%s` заборонено у workflow —",
60
65
  "мігровано на knip (js-lint.mdc, ga.mdc)",
@@ -147,6 +152,28 @@ deny contains msg if {
147
152
  msg := "concurrency.cancel-in-progress має бути true (ga.mdc)"
148
153
  }
149
154
 
155
+ # ── deny: мінімальні версії marketplace actions у `uses:` ─────────────────
156
+ #
157
+ # Канон — `template/uses-min-versions.snippet.json` (через --data).
158
+ # Перевіряє semver-подібні теги `vX.Y.Z` / `vN`; SHA-pin (40 hex) пропускаємо.
159
+
160
+ deny contains msg if {
161
+ some entry in all_flat_steps
162
+ uses := object.get(entry.step, "uses", "")
163
+ uses != ""
164
+ some action_slug, min_ver in data.template.snippet
165
+ action_uses_matches(uses, action_slug)
166
+ ref := action_uses_ref(uses)
167
+ not action_ref_meets_min(ref, min_ver)
168
+ msg := sprintf(min_uses_version_template, [
169
+ entry.job_id,
170
+ entry.step_index,
171
+ action_slug,
172
+ min_ver,
173
+ ref,
174
+ ])
175
+ }
176
+
150
177
  # ── helpers ────────────────────────────────────────────────────────────────
151
178
 
152
179
  # Об'єднаний рядок `uses` + `run` для одного кроку — для substring-пошуку
@@ -182,3 +209,65 @@ has_checkout_before(job, before) if {
182
209
  uses := object.get(step, "uses", "")
183
210
  contains(uses, "actions/checkout@")
184
211
  }
212
+
213
+ # `uses:` починається з `owner/repo@` для заданого slug.
214
+ action_uses_matches(uses, slug) if {
215
+ startswith(uses, concat("", [slug, "@"]))
216
+ }
217
+
218
+ # Ref після останнього `@` у `uses:` (owner/repo@ref).
219
+ action_uses_ref(uses) := ref if {
220
+ parts := split(uses, "@")
221
+ count(parts) >= 2
222
+ ref := parts[count(parts) - 1]
223
+ }
224
+
225
+ # SHA-pin — semver-політика не застосовується.
226
+ action_ref_is_sha_pin(ref) if {
227
+ regex.match(`^[0-9a-fA-F]{40}$`, ref)
228
+ }
229
+
230
+ # Semver ref >= min (обидва як X.Y.Z після optional `v`).
231
+ action_ref_meets_min(ref, _) if {
232
+ action_ref_is_sha_pin(ref)
233
+ }
234
+
235
+ action_ref_meets_min(ref, min_ver) if {
236
+ not action_ref_is_sha_pin(ref)
237
+ version_triple_gte(version_triple(ref), version_triple(min_ver))
238
+ }
239
+
240
+ version_triple(raw) := [major, minor, patch] if {
241
+ stripped := trim_prefix(trim_prefix(raw, "v"), "V")
242
+ parts := split_to_numbers(stripped)
243
+ major := version_part(parts, 0)
244
+ minor := version_part(parts, 1)
245
+ patch := version_part(parts, 2)
246
+ }
247
+
248
+ version_part(parts, idx) := parts[idx] if {
249
+ count(parts) > idx
250
+ }
251
+
252
+ else := 0
253
+
254
+ version_triple_gte(a, b) if {
255
+ a[0] > b[0]
256
+ }
257
+
258
+ version_triple_gte(a, b) if {
259
+ a[0] == b[0]
260
+ a[1] > b[1]
261
+ }
262
+
263
+ version_triple_gte(a, b) if {
264
+ a[0] == b[0]
265
+ a[1] == b[1]
266
+ a[2] >= b[2]
267
+ }
268
+
269
+ split_to_numbers(spec) := nums if {
270
+ tokens := regex.split(`\D+`, spec)
271
+ non_empty := [t | some t in tokens; t != ""]
272
+ nums := [n | some t in non_empty; n := to_number(t)]
273
+ }
@@ -15,7 +15,7 @@ import {
15
15
  isGqlScanSourceFile,
16
16
  shouldSkipFileForGqlScan,
17
17
  sourceFileHasGqlTaggedTemplate
18
- } from '../../../../scripts/utils/graphql-gql-scan.mjs'
18
+ } from './graphql-gql-scan.mjs'
19
19
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
20
20
  import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
21
21
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
@@ -1,18 +1,45 @@
1
1
  /**
2
2
  * Пошук tagged template **`gql\`…\``** у джерелах для правила graphql.mdc.
3
3
  *
4
- * Для **`.vue`** береться лише вміст `<script>` / `<script setup>` (спільна логіка з **vue-forbidden-imports**).
4
+ * Для **`.vue`** береться лише вміст `<script>` / `<script setup>` (паралельна реалізація екстрактора;
5
+ * аналог у `rules/vue/fix/packages/vue-forbidden-imports.mjs` — модулі не діляться кодом, щоб уникати
6
+ * cross-rule імпортів і тримати кожне правило самодостатнім).
5
7
  * Семантику визначає **oxc-parser** (`program`): рекурсивний обхід AST, збіг лише для **Identifier** з іменем **`gql`** як тега шаблону.
6
8
  */
7
9
  import { parseSync } from 'oxc-parser'
8
10
 
9
- import {
10
- contentForVueImportScan,
11
- isVueImportScanSourceFile,
12
- shouldSkipFileForVueImportScan
13
- } from './vue-forbidden-imports.mjs'
14
-
15
11
  const VUE_EXTENSION_RE = /\.vue$/u
12
+ const SOURCE_FILE_RE = /\.(vue|[cm]?[jt]sx?)$/u
13
+ const VUE_SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
14
+
15
+ /**
16
+ * Витягує з SFC лише код усередині `<script>` блоків, ігноруючи `<template>` і `<style>`.
17
+ * @param {string} sfc вміст .vue файлу
18
+ * @returns {string} конкатенований текст усіх блоків `<script>` (через подвійний переклад рядка)
19
+ */
20
+ function extractVueScriptBlocks(sfc) {
21
+ const chunks = []
22
+ VUE_SCRIPT_BLOCK_RE.lastIndex = 0
23
+ let m = VUE_SCRIPT_BLOCK_RE.exec(sfc)
24
+ while (m) {
25
+ chunks.push(m[1])
26
+ m = VUE_SCRIPT_BLOCK_RE.exec(sfc)
27
+ }
28
+ return chunks.join('\n\n')
29
+ }
30
+
31
+ /**
32
+ * Підбирає текст для сканування: для .vue — лише script-блоки, інакше — увесь вміст.
33
+ * @param {string} content сирий вміст файлу
34
+ * @param {string} filePath відносний шлях (для вибору режиму)
35
+ * @returns {string} текст для `parseSync`
36
+ */
37
+ function contentForGqlScan(content, filePath) {
38
+ if (filePath.endsWith('.vue')) {
39
+ return extractVueScriptBlocks(content)
40
+ }
41
+ return content
42
+ }
16
43
 
17
44
  /**
18
45
  * Мова для Oxc за шляхом файлу (розширення).
@@ -81,7 +108,7 @@ function astContainsGqlTag(node) {
81
108
  * @returns {boolean} true, якщо знайдено `gql`…``
82
109
  */
83
110
  export function sourceFileHasGqlTaggedTemplate(content, relativePath) {
84
- const scan = contentForVueImportScan(content, relativePath)
111
+ const scan = contentForGqlScan(content, relativePath)
85
112
  const pathForLang = virtualPathForParse(relativePath)
86
113
  const lang = langFromPath(pathForLang)
87
114
  try {
@@ -96,19 +123,27 @@ export function sourceFileHasGqlTaggedTemplate(content, relativePath) {
96
123
  }
97
124
 
98
125
  /**
99
- * Чи підлягає файл скануванню за розширенням (узгоджено з vue-import scan).
126
+ * Чи підлягає файл скануванню за розширенням (`.vue`, `.[cm]?[jt]sx?`).
100
127
  * @param {string} relativePath відносний шлях
101
128
  * @returns {boolean} true якщо файл підлягає скануванню
102
129
  */
103
130
  export function isGqlScanSourceFile(relativePath) {
104
- return isVueImportScanSourceFile(relativePath)
131
+ return SOURCE_FILE_RE.test(relativePath)
105
132
  }
106
133
 
107
134
  /**
108
- * Чи пропустити файл (декларації, auto-imports) — ті самі критерії, що для vue-import scan.
135
+ * Чи пропустити файл (декларації, auto-imports/components.d.ts) — типові generated-файли,
136
+ * у яких немає авторського коду з gql-тегами.
109
137
  * @param {string} relativePosix шлях з posix-слешами
110
138
  * @returns {boolean} true якщо файл потрібно пропустити
111
139
  */
112
140
  export function shouldSkipFileForGqlScan(relativePosix) {
113
- return shouldSkipFileForVueImportScan(relativePosix)
141
+ const base = relativePosix.split('/').pop() || ''
142
+ if (base === 'auto-imports.d.ts' || base === 'components.d.ts') {
143
+ return true
144
+ }
145
+ if (relativePosix.endsWith('.d.ts')) {
146
+ return true
147
+ }
148
+ return false
114
149
  }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Запускається лише якщо в кореневому `package.json` поле `repository`
7
7
  * вказує на `https://github.com/nitra/...` або `https://github.com/abinbevefes/...`
8
- * (інші репозиторії пропускаються без помилок — як у check-abie).
8
+ * (інші репозиторії пропускаються без помилок — як у abie-перевірках).
9
9
  *
10
10
  * Очікуваний формат URL — кластерний DNS-суфікс `<cluster>.internal`:
11
11
  * - GKE / GCP: `http://<service>.<namespace>.svc.<cluster>.internal:<port>`
@@ -27,7 +27,7 @@ import {
27
27
  parseProgramOrNull,
28
28
  templateQuasisText,
29
29
  walkAstWithAncestors
30
- } from './ast-scan-utils.mjs'
30
+ } from '../../../../scripts/utils/ast-scan-utils.mjs'
31
31
 
32
32
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
33
33
  const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
@@ -48,7 +48,7 @@ import {
48
48
  isBunSqlScanSourceFile,
49
49
  textHasBunSqlImport,
50
50
  textHasPgLibImport
51
- } from '../../../../scripts/utils/bun-sql-scan.mjs'
51
+ } from './bun-sql-scan.mjs'
52
52
  import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-json-paths.mjs'
53
53
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
54
54
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
@@ -2,7 +2,7 @@
2
2
  * Перевіряє лінт JavaScript за правилом js-lint.mdc.
3
3
  *
4
4
  * Flat ESLint з getConfig і ignore для auto-imports,
5
- * `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
5
+ * `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/rules/js-lint/fix/tooling/oxlint-canonical.json`):
6
6
  * plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
7
7
  * globals, ignorePatterns. Також перевіряє workspace `package.json` на `type: "module"`
8
8
  * і `engines`, workflow-дубль у `lint.yml`, `knip.json` autofill і застарілі `.eslintrc*`.
@@ -20,24 +20,12 @@ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mj
20
20
  /** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
21
21
  export const OXLINT_CANONICAL_JSON_PATH = join(
22
22
  dirname(fileURLToPath(import.meta.url)),
23
- '..',
24
- '..',
25
- '..',
26
- '..',
27
- 'scripts',
28
- 'utils',
29
23
  'oxlint-canonical.json'
30
24
  )
31
25
 
32
26
  /** Шлях до канонічного knip JSON у цьому пакеті — копіюється у корінь проєкту-споживача, якщо відсутній. */
33
27
  export const KNIP_CANONICAL_JSON_PATH = join(
34
28
  dirname(fileURLToPath(import.meta.url)),
35
- '..',
36
- '..',
37
- '..',
38
- '..',
39
- 'scripts',
40
- 'utils',
41
29
  'knip-canonical.json'
42
30
  )
43
31
 
@@ -166,7 +154,7 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
166
154
 
167
155
  if (!deepEqualOxlintCanonical(actual, expected)) {
168
156
  failures.push(
169
- `.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/scripts/utils/oxlint-canonical.json)`
157
+ `.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js-lint/fix/tooling/oxlint-canonical.json)`
170
158
  )
171
159
  }
172
160
  }
@@ -396,7 +384,7 @@ async function checkKnipConfig(passFn, failFn) {
396
384
  return
397
385
  }
398
386
  await copyFile(KNIP_CANONICAL_JSON_PATH, 'knip.json')
399
- passFn('knip.json створено з канонічного npm/scripts/utils/knip-canonical.json (js-lint.mdc)')
387
+ passFn('knip.json створено з канонічного npm/rules/js-lint/fix/tooling/knip-canonical.json (js-lint.mdc)')
400
388
  }
401
389
 
402
390
  /**
@@ -2,7 +2,7 @@
2
2
  * Збирає `oxlint-canonical.json` з `oxlint-canonical-skeleton.json` (без поля rules) та списку
3
3
  * правил у `oxlint-rules.tsv` (колонки: ім’я правила, TAB, severity: deny | off | error).
4
4
  *
5
- * Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./scripts/utils/rebuild-oxlint-canonical.mjs`,
5
+ * Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/fix/tooling/rebuild-oxlint-canonical.mjs`,
6
6
  * потім скопіюй оновлений канон у корінь споживача як `.oxlintrc.json` за потреби.
7
7
  */
8
8
  import { readFileSync, writeFileSync } from 'node:fs'
@@ -25,7 +25,7 @@ version: '1.23'
25
25
 
26
26
  У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
27
27
 
28
- У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/scripts/utils/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Оновити канон можна з репозиторію пакета або скопіювавши файл після **`bun ./scripts/utils/rebuild-oxlint-canonical.mjs`** (джерело правил — **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
28
+ У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js-lint/fix/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Оновити канон можна з репозиторію пакета або скопіювавши файл після **`bun ./rules/js-lint/fix/tooling/rebuild-oxlint-canonical.mjs`** (джерело правил — **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
29
29
 
30
30
  Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
31
31
 
@@ -62,7 +62,7 @@ version: '1.23'
62
62
 
63
63
  Перевірку невикористаних залежностей і експортів виконує **knip** (заміна `depcheck`). Викликається у скрипті `lint-js` і в CI разом з oxlint/eslint/jscpd — окремий крок у CI не потрібен.
64
64
 
65
- У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/scripts/utils/knip-canonical.json`](../scripts/utils/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `depcheck`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
65
+ У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js-lint/fix/tooling/knip-canonical.json`](./fix/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `depcheck`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
66
66
 
67
67
  Якщо `knip.json` відсутній — `npx @nitra/cursor check js-lint` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
68
68
 
@@ -22,7 +22,7 @@ import {
22
22
  findUnsafeMssqlInListUnparsedInText,
23
23
  findUnsafeMssqlInListMissingEmptyGuardInText,
24
24
  isMssqlScanSourceFile
25
- } from '../../../../scripts/utils/mssql-pool-scan.mjs'
25
+ } from './mssql-pool-scan.mjs'
26
26
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
27
27
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
28
28
 
@@ -30,7 +30,7 @@ import {
30
30
  normalizeSnippet,
31
31
  offsetToLine,
32
32
  walkAstWithAncestors
33
- } from './ast-scan-utils.mjs'
33
+ } from '../../../../scripts/utils/ast-scan-utils.mjs'
34
34
 
35
35
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
36
36
  const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
@@ -18,7 +18,7 @@ import {
18
18
  offsetToLine,
19
19
  requireCallModule,
20
20
  walkAstWithAncestors
21
- } from './ast-scan-utils.mjs'
21
+ } from '../../../../scripts/utils/ast-scan-utils.mjs'
22
22
 
23
23
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
24
24
  const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
@@ -29,7 +29,7 @@
29
29
  * Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
30
30
  * бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
31
31
  */
32
- import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-scan-utils.mjs'
32
+ import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '../../../../scripts/utils/ast-scan-utils.mjs'
33
33
 
34
34
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
35
35
  const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
@@ -40,22 +40,22 @@ import {
40
40
  findBunyanImportsInText,
41
41
  isBunyanScanSourceFile,
42
42
  shouldSkipFileForBunyanScan
43
- } from '../../../../scripts/utils/bunyan-imports.mjs'
44
- import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '../../../../scripts/utils/check-env-scan.mjs'
43
+ } from './bunyan-imports.mjs'
44
+ import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './check-env-scan.mjs'
45
45
  import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
46
46
  import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
47
- import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '../../../../scripts/utils/conn-file-rules.mjs'
47
+ import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './conn-file-rules.mjs'
48
48
  import {
49
49
  findConnFactoryImportsInText,
50
50
  isConnImportsScanSourceFile,
51
51
  isInsideConnDir,
52
52
  resolveConnDirFromPackageJson
53
- } from '../../../../scripts/utils/conn-imports-scan.mjs'
53
+ } from './conn-imports-scan.mjs'
54
54
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
55
55
  import {
56
56
  findPromiseSetTimeoutInText,
57
57
  isPromiseSetTimeoutScanSourceFile
58
- } from '../../../../scripts/utils/promise-settimeout-scan.mjs'
58
+ } from './promise-settimeout-scan.mjs'
59
59
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
60
60
  import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
61
61
 
@@ -14,7 +14,7 @@
14
14
  * Парсимо через oxc-parser; коли файл не парситься — повертаємо порожні результати, щоб
15
15
  * не змішувати помилки синтаксису з порушеннями цього правила.
16
16
  */
17
- import { parseProgramOrNull } from './ast-scan-utils.mjs'
17
+ import { parseProgramOrNull } from '../../../../scripts/utils/ast-scan-utils.mjs'
18
18
 
19
19
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
20
20
 
@@ -16,7 +16,7 @@
16
16
  * використовується. Якщо файл не парситься — повертаємо порожній результат, спочатку
17
17
  * треба полагодити синтаксис.
18
18
  */
19
- import { langFromPath, normalizeSnippet, offsetToLine } from './ast-scan-utils.mjs'
19
+ import { langFromPath, normalizeSnippet, offsetToLine } from '../../../../scripts/utils/ast-scan-utils.mjs'
20
20
  import { parseSync } from 'oxc-parser'
21
21
 
22
22
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
@@ -12,7 +12,7 @@
12
12
  * Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається
13
13
  * порожній результат (як інші сканери — спочатку треба полагодити синтаксис).
14
14
  */
15
- import { normalizeSnippet, offsetToLine, parseProgramOrNull } from './ast-scan-utils.mjs'
15
+ import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '../../../../scripts/utils/ast-scan-utils.mjs'
16
16
 
17
17
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
18
18
 
package/rules/k8s/k8s.mdc CHANGED
@@ -375,7 +375,7 @@ images:
375
375
 
376
376
  Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
377
377
 
378
- **`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — **`check-abie.mjs`** / **abie.mdc**.
378
+ **`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — Rego-пакет **`abie.health_check_policy`** + **abie.mdc**.
379
379
 
380
380
  За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
381
381
 
@@ -453,6 +453,11 @@ export function findTestFrameworkImport(content, virtualPath) {
453
453
  * Класифікує опублікований файл як test/fixture, якщо хоча б одна з ознак:
454
454
  * (1) у шляху є каталог із `TEST_DIR_NAMES`; (2) basename відповідає
455
455
  * `TEST_FILE_PATTERNS`; (3) для JS/TS-розширень — імпорт test-фреймворку.
456
+ *
457
+ * Carve-out: для шляху `rules/<rule-name>/...` сегмент `<rule-name>` (індекс 1)
458
+ * — це ім'я правила, а не каталог. Зокрема, правило з id `test` (або `tests`)
459
+ * описує конвенцію розміщення тестів і саме по собі не є test-fixture'ом.
460
+ * Подальші сегменти (наприклад, `rules/<r>/fix/<c>/tests/`) продовжують перевірятись.
456
461
  * @param {string} relPath posix-шлях відносно `npm/`
457
462
  * @returns {Promise<string | null>} причина порушення або `null`
458
463
  */
@@ -460,7 +465,12 @@ export async function classifyPublishedFileAsTest(relPath) {
460
465
  const segments = relPath.split('/')
461
466
  const base = segments.at(-1)
462
467
  const dirs = segments.slice(0, -1)
463
- const testDir = dirs.find(seg => TEST_DIR_NAMES.has(seg.toLowerCase()))
468
+ const testDir = dirs.find((seg, idx) => {
469
+ if (idx === 1 && dirs[0] === 'rules') {
470
+ return false
471
+ }
472
+ return TEST_DIR_NAMES.has(seg.toLowerCase())
473
+ })
464
474
  if (testDir) return `test-style каталог "${testDir}/"`
465
475
  if (TEST_FILE_PATTERNS.some(re => re.test(base))) return `test-style ім'я файлу`
466
476
  if (JS_LIKE_EXT_RE.test(base)) {
@@ -0,0 +1 @@
1
+ якщо у проекті є хоча б один файл `*.test.mjs`
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Перевіряє, що всі `*.test.mjs` лежать у каталозі `tests/` (а не поряд із джерельним файлом).
3
+ *
4
+ * Конвенція (test.mdc): `dir/foo.mjs` → тест у `dir/tests/foo.test.mjs`.
5
+ * `*_test.rego` виключені: Rego unit-тести живуть поряд із полісі (OPA community convention).
6
+ *
7
+ * Пропускає: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv` (через `walkDir`)
8
+ * і шляхи з `.n-cursor.json:ignore`.
9
+ */
10
+ import { basename, dirname, relative } from 'node:path'
11
+
12
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
13
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
14
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
15
+
16
+ const TESTS_DIR_NAME = 'tests'
17
+
18
+ /**
19
+ * Чи файл є JS-тестом (`*.test.mjs`).
20
+ * @param {string} absPath абсолютний шлях
21
+ * @returns {boolean}
22
+ */
23
+ function isTestFile(absPath) {
24
+ return basename(absPath).endsWith('.test.mjs')
25
+ }
26
+
27
+ /**
28
+ * Перевіряє, чи лежить тест у каталозі з іменем `tests`.
29
+ * @param {string} absPath абсолютний шлях до тесту
30
+ * @returns {boolean} `true`, якщо басенейм батьківської директорії — `tests`
31
+ */
32
+ function isInsideTestsDir(absPath) {
33
+ return basename(dirname(absPath)) === TESTS_DIR_NAME
34
+ }
35
+
36
+ /**
37
+ * Перевіряє розміщення тестових файлів у каталозі `tests/` (test.mdc).
38
+ * @returns {Promise<number>} 0 — всі тести у `tests/`, 1 — є порушення
39
+ */
40
+ export async function check() {
41
+ const reporter = createCheckReporter()
42
+ const { pass, fail } = reporter
43
+
44
+ const cwd = process.cwd()
45
+ const ignorePaths = await loadCursorIgnorePaths(cwd)
46
+
47
+ /** @type {string[]} */
48
+ const offenders = []
49
+ let totalTests = 0
50
+
51
+ await walkDir(
52
+ cwd,
53
+ absPath => {
54
+ if (!isTestFile(absPath)) {
55
+ return
56
+ }
57
+ totalTests++
58
+ if (!isInsideTestsDir(absPath)) {
59
+ offenders.push(relative(cwd, absPath))
60
+ }
61
+ },
62
+ ignorePaths
63
+ )
64
+
65
+ if (offenders.length === 0) {
66
+ pass(`Всі ${totalTests} файлів *.test.mjs у каталозі tests/ (test.mdc)`)
67
+ return reporter.getExitCode()
68
+ }
69
+
70
+ for (const offenderPath of offenders) {
71
+ const parentDir = dirname(offenderPath)
72
+ const base = basename(offenderPath)
73
+ fail(`${offenderPath}: тест має лежати у tests/ — перенеси у ${parentDir}/${TESTS_DIR_NAME}/${base} (test.mdc)`)
74
+ }
75
+
76
+ return reporter.getExitCode()
77
+ }
@@ -0,0 +1,60 @@
1
+ ---
2
+ description: JS-тести (*.test.mjs) живуть у каталозі tests/ поряд із джерельним файлом, а не безпосередньо в тій же директорії
3
+ version: '1.1'
4
+ alwaysApply: true
5
+ ---
6
+
7
+ ## Конвенція розміщення тестів
8
+
9
+ JS-тести у пакеті лежать у **піддиректорії `tests/`** поряд із кодом, який тестується, а **не безпосередньо біля джерельного файлу**.
10
+
11
+ ```
12
+ rules/foo/fix/bar/
13
+ ├── check.mjs ← JS-джерело
14
+ └── tests/
15
+ └── check.test.mjs ← тест check.mjs
16
+ ```
17
+
18
+ **Чому так:**
19
+
20
+ - Каталог-з-кодом залишається чистим: продакшен-модулі, политики, темплейти не миготять серед тестових файлів.
21
+ - Один погляд на дерево показує, що піде у npm tarball (все, крім `tests/` і `__fixtures__/` — їх ловить `package.json#files` через негативні globs).
22
+ - Якщо тест переростає у мульти-файловий — він уже у власному каталозі, без хаотичного розкидання.
23
+
24
+ **Виняток — Rego unit-тести (`*_test.rego`):** лишаються **поряд із полісі-файлом** відповідно до загальноприйнятого OPA/Conftest community-патерну. `conftest verify -p <dir>` рекурсивно підхоплює їх незалежно від місця в каталозі.
25
+
26
+ ```
27
+ rules/foo/policy/package_json/
28
+ ├── package_json.rego ← Rego-правило
29
+ ├── package_json_test.rego ← Rego unit-тести (поряд, не у tests/)
30
+ └── target.json
31
+ ```
32
+
33
+ **Спеціальні випадки:**
34
+
35
+ - **Integration-тести на рівні пакета** — у `<root>/tests/` (наприклад `npm/tests/`). Це не «біля файлу», а виокремлений каталог для тестів, що покривають весь пакет.
36
+ - **Fixtures**: `__fixtures__/` (для shared) і `fixtures/` (для rule-specific) також живуть **усередині `tests/`**: `tests/__fixtures__/...` і `tests/fixtures/...`.
37
+ - **Test helpers** (`test-helpers.mjs`) можуть лишатися у `scripts/utils/` як shared infra — конвенція стосується файлів `*.test.mjs`.
38
+
39
+ **Гарантії tarball-чистоти** через `package.json#files`:
40
+
41
+ ```json
42
+ "files": [
43
+ "...",
44
+ "!**/*.test.mjs",
45
+ "!**/*_test.rego",
46
+ "!**/__fixtures__/**",
47
+ "!**/fixtures/**",
48
+ "!**/test-helpers.mjs"
49
+ ]
50
+ ```
51
+
52
+ Recursive globs ловлять файли всередині `tests/` так само, як ловили б їх біля джерела.
53
+
54
+ ## Що перевіряє правило
55
+
56
+ `npx @nitra/cursor check test` (concern `location`) проходить деревом пакета й перевіряє: **кожен `*.test.mjs` файл лежить усередині каталогу з ім'ям `tests`** (точне співпадіння басенейму батьківської директорії). Файли поза цим каталогом репортуються з порадою куди їх перенести.
57
+
58
+ `*_test.rego` перевіркою **не охоплюються** — вони не переміщуються.
59
+
60
+ Пропускаються: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`, шляхи з `.n-cursor.json:ignore`.
@@ -15,7 +15,7 @@
15
15
  * натомість використовуй `mode` з `defineConfig(({ mode }) => ...)`.
16
16
  *
17
17
  * Заборонені явні value-імпорти з `vue` у джерелах пакета — сканування `.vue`/`.ts`/`.js` тощо
18
- * через **oxc-parser** (`module.staticImports`; див. `utils/vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
18
+ * через **oxc-parser** (`module.staticImports`; див. `./vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
19
19
  *
20
20
  * Окремо в `.vue` SFC заборонено імпорти Node-нативних модулів — `node:*` префікс або bare-ім’я
21
21
  * вбудованого модуля Node (`fs`, `path`, `timers/promises` тощо). Vue SFC виконується у браузері,
@@ -31,7 +31,7 @@ import {
31
31
  findForbiddenVueImportsInSourceFile,
32
32
  isVueImportScanSourceFile,
33
33
  shouldSkipFileForVueImportScan
34
- } from '../../../../scripts/utils/vue-forbidden-imports.mjs'
34
+ } from './vue-forbidden-imports.mjs'
35
35
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
36
36
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
37
37
  import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
package/rules/vue/vue.mdc CHANGED
@@ -341,4 +341,4 @@ import path from 'node:path'
341
341
 
342
342
  ## Перевірка
343
343
 
344
- `npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/scripts/utils/vue-forbidden-imports.mjs`).
344
+ `npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/rules/vue/fix/packages/vue-forbidden-imports.mjs`).