@nitra/cursor 1.10.0 → 1.11.1

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 (98) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/bin/n-cursor.js +31 -31
  3. package/package.json +2 -4
  4. package/rules/abie/js/applies/check.mjs +24 -0
  5. package/rules/abie/js/env_dns/check.mjs +53 -0
  6. package/rules/abie/js/firebase_hosting/check.mjs +49 -0
  7. package/rules/abie/js/hc_pairing/check.mjs +58 -0
  8. package/rules/abie/js/ua_http_route/check.mjs +86 -0
  9. package/rules/abie/js/ua_node_selector/check.mjs +65 -0
  10. package/rules/abie/policy/base_deployment_preem/target.json +10 -0
  11. package/rules/abie/policy/clean_merged_ignore_branches/target.json +4 -0
  12. package/rules/abie/policy/health_check_policy/target.json +4 -0
  13. package/rules/abie/policy/http_route_base/target.json +4 -0
  14. package/rules/abie/utils/enabled.mjs +35 -0
  15. package/rules/abie/utils/env-dns.mjs +81 -0
  16. package/rules/abie/utils/hc-yaml.mjs +27 -0
  17. package/rules/abie/utils/http-route.mjs +93 -0
  18. package/rules/abie/utils/k8s-tree.mjs +102 -0
  19. package/rules/abie/utils/kustomization-patches.mjs +224 -0
  20. package/rules/abie/utils/overlay-paths.mjs +97 -0
  21. package/rules/abie/utils/yaml.mjs +72 -0
  22. package/rules/adr/js/{check.mjs → hooks/check.mjs} +2 -2
  23. package/rules/adr/policy/settings_json/target.json +4 -0
  24. package/rules/adr/policy/settings_local_json/target.json +4 -0
  25. package/rules/bun/js/{check.mjs → layout/check.mjs} +1 -1
  26. package/rules/bun/policy/bunfig/target.json +4 -0
  27. package/rules/bun/policy/package_json/target.json +4 -0
  28. package/rules/capacitor/js/{check.mjs → platforms/check.mjs} +1 -1
  29. package/rules/capacitor/policy/package_json/target.json +4 -0
  30. package/rules/changelog/js/{check.mjs → consistency/check.mjs} +2 -2
  31. package/rules/docker/js/{check.mjs → lint/check.mjs} +5 -5
  32. package/rules/docker/policy/lint_docker_yml/target.json +4 -0
  33. package/rules/docker/policy/package_json/target.json +4 -0
  34. package/rules/ga/js/lint.mjs +1 -1
  35. package/rules/ga/js/{check.mjs → workflows/check.mjs} +4 -4
  36. package/rules/graphql/js/{check.mjs → tooling/check.mjs} +5 -5
  37. package/rules/hasura/js/{check.mjs → internal_urls/check.mjs} +4 -4
  38. package/rules/hasura/policy/svc_hl/target.json +4 -0
  39. package/rules/image-avif/js/{check.mjs → avif_generation/check.mjs} +5 -5
  40. package/rules/image-avif/policy/package_json/target.json +4 -0
  41. package/rules/image-compress/js/{check.mjs → package_setup/check.mjs} +1 -1
  42. package/rules/image-compress/policy/package_json/target.json +4 -0
  43. package/rules/js-bun-db/js/{check.mjs → safety/check.mjs} +5 -5
  44. package/rules/js-bun-db/policy/package_json/target.json +4 -0
  45. package/rules/js-bun-redis/js/{check.mjs → imports/check.mjs} +4 -4
  46. package/rules/js-bun-redis/policy/package_json/target.json +4 -0
  47. package/rules/js-lint/js/{check.mjs → tooling/check.mjs} +16 -2
  48. package/rules/js-lint/policy/lint_js_yml/target.json +4 -0
  49. package/rules/js-lint/policy/package_json/target.json +4 -0
  50. package/rules/js-mssql/js/{check.mjs → deps/check.mjs} +5 -5
  51. package/rules/js-mssql/policy/package_json/target.json +4 -0
  52. package/rules/js-run/js/{check.mjs → runtime/check.mjs} +10 -10
  53. package/rules/js-run/policy/configmap/target.json +4 -0
  54. package/rules/js-run/policy/package_json/target.json +4 -0
  55. package/rules/k8s/js/{check.mjs → manifests/check.mjs} +4 -4
  56. package/rules/k8s/policy/base_kustomization/target.json +4 -0
  57. package/rules/k8s/policy/base_manifest/target.json +10 -0
  58. package/rules/k8s/policy/gateway/target.json +4 -0
  59. package/rules/k8s/policy/hpa_pdb/target.json +4 -0
  60. package/rules/k8s/policy/kustomization/target.json +4 -0
  61. package/rules/k8s/policy/manifest/target.json +4 -0
  62. package/rules/k8s/policy/svc_hl_yaml/target.json +4 -0
  63. package/rules/k8s/policy/svc_yaml/target.json +4 -0
  64. package/rules/nginx-default-tpl/js/{check.mjs → template/check.mjs} +5 -5
  65. package/rules/npm-module/js/{check.mjs → package_structure/check.mjs} +4 -4
  66. package/rules/npm-module/policy/emit_types_config/target.json +4 -0
  67. package/rules/npm-module/policy/npm_package_json/target.json +4 -0
  68. package/rules/npm-module/policy/npm_publish_yml/target.json +4 -0
  69. package/rules/npm-module/policy/root_package_json/target.json +4 -0
  70. package/rules/php/js/{check.mjs → tooling/check.mjs} +1 -1
  71. package/rules/php/policy/lint_php_yml/target.json +4 -0
  72. package/rules/php/policy/package_json/target.json +4 -0
  73. package/rules/rego/js/applies/check.mjs +54 -0
  74. package/rules/rego/policy/package_json/target.json +5 -0
  75. package/rules/rego/policy/vscode_extensions/target.json +5 -0
  76. package/rules/rego/policy/vscode_settings/target.json +5 -0
  77. package/rules/style-lint/js/{check.mjs → tooling/check.mjs} +1 -1
  78. package/rules/style-lint/policy/lint_style_yml/target.json +4 -0
  79. package/rules/style-lint/policy/package_json/target.json +4 -0
  80. package/rules/style-lint/policy/vscode_extensions/target.json +4 -0
  81. package/rules/style-lint/policy/vscode_settings/target.json +4 -0
  82. package/rules/tauri/js/{check.mjs → tooling/check.mjs} +2 -2
  83. package/rules/text/js/{check.mjs → formatting/check.mjs} +2 -2
  84. package/rules/text/policy/cspell/target.json +4 -0
  85. package/rules/text/policy/markdownlint/target.json +4 -0
  86. package/rules/text/policy/oxfmtrc/target.json +4 -0
  87. package/rules/text/policy/package_json/target.json +4 -0
  88. package/rules/text/policy/vscode_extensions/target.json +4 -0
  89. package/rules/text/policy/vscode_settings/target.json +4 -0
  90. package/rules/vue/js/{check.mjs → packages/check.mjs} +5 -5
  91. package/rules/vue/policy/package_json/target.json +4 -0
  92. package/schemas/target.json +58 -0
  93. package/scripts/lint-conftest.mjs +65 -414
  94. package/scripts/utils/discover-checkable-rules.mjs +106 -0
  95. package/scripts/utils/resolve-target-files.mjs +109 -0
  96. package/scripts/utils/run-rule.mjs +127 -0
  97. package/rules/abie/js/check.mjs +0 -1152
  98. package/rules/rego/js/check.mjs +0 -106
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Резолвер списку файлів для одного `policy/<name>/target.json` у новій структурі правил.
3
+ *
4
+ * Дві форми у `target.json:files`:
5
+ * - `{ "single": "<rel>" }` — конкретний відносний шлях. Якщо `existsSync(root/single)` → `[single]`;
6
+ * інакше `[]` (caller сам вирішує fail vs silent skip за `required`).
7
+ * - `{ "walkGlob": <glob | glob[]> }` — picomatch проти posix-відносних шляхів, отриманих обходом
8
+ * `walkDir` від `root` із загальними skip-ами та `.n-cursor.json:ignore`. Обхід кешований у
9
+ * `walkCache` (Map ключ — підпис ignorePaths) — повторні таргети з тим самим набором ignore
10
+ * перевикористовують список без нового readdir.
11
+ *
12
+ * Path-traversal у `single` — кидаємо помилку при resolve. Реалізує інваріант контракту: полісі
13
+ * читають лише файли в репо.
14
+ */
15
+ import { existsSync } from 'node:fs'
16
+ import { isAbsolute, join, normalize, relative, sep } from 'node:path'
17
+
18
+ import picomatch from 'picomatch'
19
+
20
+ import { loadCursorIgnorePaths } from './load-cursor-config.mjs'
21
+ import { walkDir } from './walkDir.mjs'
22
+
23
+ /** Узгоджений regex для path-traversal: `..` як сегмент або абсолютний шлях. */
24
+ const PARENT_SEGMENT_RE = /(^|[\\/])\.\.([\\/]|$)/u
25
+
26
+ /**
27
+ * Перевіряє, що `single`-шлях у `target.json:files` лежить у межах репозиторію.
28
+ * Кидає помилку, якщо шлях абсолютний або містить сегмент `..`.
29
+ * @param {string} singlePath значення `files.single`
30
+ * @returns {void}
31
+ */
32
+ function assertSafeSinglePath(singlePath) {
33
+ if (isAbsolute(singlePath)) {
34
+ throw new Error(`target.json: files.single має бути відносним шляхом (отримано: ${singlePath})`)
35
+ }
36
+ if (PARENT_SEGMENT_RE.test(singlePath)) {
37
+ throw new Error(`target.json: files.single не може містити '..' (отримано: ${singlePath})`)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Збирає всі файли (posix-відносні шляхи від `root`) одним обходом дерева.
43
+ * Скіпи: загальні з `walkDir` + `.n-cursor.json:ignore`.
44
+ * @param {string} root абсолютний корінь репозиторію
45
+ * @param {string[]} ignorePaths абсолютні posix-шляхи виключених каталогів
46
+ * @returns {Promise<string[]>} відсортовані posix-відносні шляхи
47
+ */
48
+ async function walkAllRelative(root, ignorePaths) {
49
+ /** @type {string[]} */
50
+ const out = []
51
+ await walkDir(
52
+ root,
53
+ abs => {
54
+ const rel = relative(root, abs).split(sep).join('/')
55
+ if (rel.length > 0) out.push(rel)
56
+ },
57
+ ignorePaths
58
+ )
59
+ return out.toSorted((a, b) => a.localeCompare(b))
60
+ }
61
+
62
+ /**
63
+ * Витягує (або обчислює і кешує) список усіх файлів у дереві для заданого набору ignore-шляхів.
64
+ * Кеш — мapа `signature → Promise<string[]>`, тож паралельні виклики одного й того ж набору
65
+ * чекають один обхід.
66
+ * @param {string} root абсолютний корінь репозиторію
67
+ * @param {string[]} ignorePaths абсолютні posix-шляхи виключених каталогів
68
+ * @param {Map<string, Promise<string[]>>} walkCache мутабельний кеш від caller-а
69
+ * @returns {Promise<string[]>} відсортовані posix-відносні шляхи
70
+ */
71
+ function getAllFilesCached(root, ignorePaths, walkCache) {
72
+ const signature = `${root}|${ignorePaths.join('|')}`
73
+ let p = walkCache.get(signature)
74
+ if (!p) {
75
+ p = walkAllRelative(root, ignorePaths)
76
+ walkCache.set(signature, p)
77
+ }
78
+ return p
79
+ }
80
+
81
+ /**
82
+ * Резолвить список файлів для одного `target.json:files`.
83
+ * @param {object} filesSpec поле `files` з `target.json` (вже після schema-валідації)
84
+ * @param {string} root абсолютний корінь репозиторію
85
+ * @param {Map<string, Promise<string[]>>} walkCache кеш обходів дерева (cross-target у межах одного check-прогону)
86
+ * @returns {Promise<string[]>} абсолютні шляхи знайдених файлів (порожній — нічого не знайдено)
87
+ */
88
+ export async function resolveTargetFiles(filesSpec, root, walkCache) {
89
+ if (typeof filesSpec?.single === 'string') {
90
+ assertSafeSinglePath(filesSpec.single)
91
+ const normalized = normalize(filesSpec.single).split(sep).join('/')
92
+ const abs = join(root, normalized)
93
+ return existsSync(abs) ? [abs] : []
94
+ }
95
+ if (filesSpec?.walkGlob !== undefined) {
96
+ const ignorePaths = await loadCursorIgnorePaths(root)
97
+ const all = await getAllFilesCached(root, ignorePaths, walkCache)
98
+ const globs = Array.isArray(filesSpec.walkGlob) ? filesSpec.walkGlob : [filesSpec.walkGlob]
99
+ // picomatch у масиві трактує `!neg` як ОКРЕМИЙ позитивний матчер «не-neg» (some-OR логіка),
100
+ // тож наївне `picomatch(['pos','!neg'])` повертає true майже на всьому. Розділяємо вручну:
101
+ // позитиви join-имо через picomatch(...), негативні фільтруємо окремим isExcluded.
102
+ const positives = globs.filter(g => !g.startsWith('!'))
103
+ const negatives = globs.filter(g => g.startsWith('!')).map(g => g.slice(1))
104
+ const isMatch = positives.length > 0 ? picomatch(positives, { dot: false }) : () => false
105
+ const isExcluded = negatives.length > 0 ? picomatch(negatives, { dot: false }) : () => false
106
+ return all.filter(rel => isMatch(rel) && !isExcluded(rel)).map(rel => join(root, rel))
107
+ }
108
+ throw new Error(`target.json: files має містити single або walkGlob (отримано: ${JSON.stringify(filesSpec)})`)
109
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Оркестратор одного правила під CLI `check`.
3
+ *
4
+ * Послідовність (concerns у межах правила — алфавітно):
5
+ * 1. **applies-гейт** з `js/applies/check.mjs`. Якщо модуль експортує `applies()` і вона повертає
6
+ * false — друкуємо `✅ правило не застосовне` і завершуємо без подальших викликів.
7
+ * 2. **JS-концерни** — кожен `check*.mjs` у `js/<concern>/`. Concern `applies` теж може мати
8
+ * `check()` для друку контексту (його `applies()` уже відпрацював на кроці 1, він не повторюється).
9
+ * 3. **Policy-концерни** — кожен `policy/<concern>/target.json` через `runConftestBatch`.
10
+ * Реcолвер `resolveTargetFiles` ділить cache (`walkCache`) між концернами.
11
+ *
12
+ * Кожен concern має власний `createCheckReporter` — їхні exit-коди OR-яться в один на рівні правила.
13
+ * Це дає той самий 0/1 контракт, що й попередня модель «один check.mjs на правило».
14
+ */
15
+ import { readFile } from 'node:fs/promises'
16
+ import { join } from 'node:path'
17
+
18
+ import { createCheckReporter } from './check-reporter.mjs'
19
+ import { resolveTargetFiles } from './resolve-target-files.mjs'
20
+ import { runConftestBatch } from './run-conftest-batch.mjs'
21
+
22
+ const APPLIES_CONCERN_NAME = 'applies'
23
+
24
+ /**
25
+ * Обчислює абсолютний шлях до файла-check у JS-концерні.
26
+ * @param {string} bundledRulesDir абсолютний `rules/`
27
+ * @param {string} ruleId id правила
28
+ * @param {import('./discover-checkable-rules.mjs').JsConcern} concern опис концерну
29
+ * @param {string} fileName імʼя файла з `concern.files`
30
+ * @returns {string} абсолютний шлях
31
+ */
32
+ function resolveJsCheckPath(bundledRulesDir, ruleId, concern, fileName) {
33
+ return join(bundledRulesDir, ruleId, 'js', concern.name, fileName)
34
+ }
35
+
36
+ /**
37
+ * Спробувати викликати applies() гейт з `js/applies/check.mjs` правила.
38
+ * Гейт активний лише за наявності концерну з імʼям `applies` і експортом-функцією `applies` у його
39
+ * першому check-файлі (алфавіт).
40
+ * @param {string} bundledRulesDir абсолютний `rules/`
41
+ * @param {import('./discover-checkable-rules.mjs').CheckableRule} rule опис правила
42
+ * @returns {Promise<boolean>} `true` — правило застосовне (або гейту немає); `false` — пропустити
43
+ */
44
+ async function evaluateAppliesGate(bundledRulesDir, rule) {
45
+ const concern = rule.jsConcerns.find(c => c.name === APPLIES_CONCERN_NAME)
46
+ if (!concern || concern.files.length === 0) return true
47
+ const path = resolveJsCheckPath(bundledRulesDir, rule.id, concern, concern.files[0])
48
+ const mod = await import(path)
49
+ if (typeof mod.applies !== 'function') return true
50
+ return Boolean(await mod.applies())
51
+ }
52
+
53
+ /**
54
+ * Запускає одну policy-полісі через `runConftestBatch`. Створює локальний репортер,
55
+ * читає `target.json`, резолвить файли, фіксує fail/pass — і повертає exit-код.
56
+ * @param {string} bundledRulesDir абсолютний `rules/`
57
+ * @param {string} ruleId id правила
58
+ * @param {string} concernName імʼя полісі (= підкаталог у `policy/`)
59
+ * @param {Map<string, Promise<string[]>>} walkCache shared cache між концернами одного check-прогону
60
+ * @returns {Promise<number>} 0 — OK, 1 — є порушення
61
+ */
62
+ async function runPolicyConcern(bundledRulesDir, ruleId, concernName, walkCache) {
63
+ const reporter = createCheckReporter()
64
+ const targetPath = join(bundledRulesDir, ruleId, 'policy', concernName, 'target.json')
65
+ /** @type {{ files: { single?: string, walkGlob?: string|string[], required?: boolean }, missingMessage?: string }} */
66
+ const target = JSON.parse(await readFile(targetPath, 'utf8'))
67
+ const files = await resolveTargetFiles(target.files, process.cwd(), walkCache)
68
+ if (files.length === 0) {
69
+ if (target.files.required && target.files.single) {
70
+ const msg =
71
+ target.missingMessage ?? `${target.files.single} не існує — створи згідно ${ruleId}.mdc (${ruleId}.${concernName})`
72
+ reporter.fail(msg)
73
+ }
74
+ return reporter.getExitCode()
75
+ }
76
+ // Rego не дозволяє '-' в імені пакета, тому kebab-id у `.n-cursor.json:rules`
77
+ // мапиться на snake у namespace. Файлова структура `rules/<id>/policy/` лишається kebab.
78
+ const regoNamespace = `${ruleId.replaceAll('-', '_')}.${concernName}`
79
+ const violations = runConftestBatch({
80
+ policyDirRel: `${ruleId}/${concernName}`,
81
+ namespace: regoNamespace,
82
+ files
83
+ })
84
+ if (violations.length === 0) {
85
+ reporter.pass(`${concernName}: ${files.length} файл(ів) OK (rego)`)
86
+ } else {
87
+ for (const v of violations) reporter.fail(v.message)
88
+ }
89
+ return reporter.getExitCode()
90
+ }
91
+
92
+ /**
93
+ * Запускає одне правило: applies-гейт → JS-концерни → policy-концерни.
94
+ * @param {import('./discover-checkable-rules.mjs').CheckableRule} rule
95
+ * @param {string} bundledRulesDir абсолютний шлях до `rules/`
96
+ * @param {Map<string, Promise<string[]>>} walkCache shared cache (один на check-прогон)
97
+ * @returns {Promise<number>} 0 — OK, 1 — є порушення в одному чи більше концернів
98
+ */
99
+ export async function runRule(rule, bundledRulesDir, walkCache) {
100
+ console.log(`📋 ${rule.id}:`)
101
+
102
+ if (!(await evaluateAppliesGate(bundledRulesDir, rule))) {
103
+ console.log(` ✅ Правило ${rule.id} не застосовне до цього репо — пропущено`)
104
+ return 0
105
+ }
106
+
107
+ let totalCode = 0
108
+
109
+ for (const concern of rule.jsConcerns) {
110
+ for (const fileName of concern.files) {
111
+ const path = resolveJsCheckPath(bundledRulesDir, rule.id, concern, fileName)
112
+ // eslint-disable-next-line no-unsanitized/method -- path будується з discovered concern/file, які пройшли regex CHECK_FILENAME_RE
113
+ const mod = await import(path)
114
+ if (typeof mod.check === 'function') {
115
+ const code = await mod.check()
116
+ if (code !== 0) totalCode = 1
117
+ }
118
+ }
119
+ }
120
+
121
+ for (const policyConcern of rule.policyConcerns) {
122
+ const code = await runPolicyConcern(bundledRulesDir, rule.id, policyConcern.name, walkCache)
123
+ if (code !== 0) totalCode = 1
124
+ }
125
+
126
+ return totalCode
127
+ }