@nitra/cursor 1.9.23 → 1.11.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.
Files changed (84) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +3 -3
  2. package/.claude-template/hooks/normalize-decisions.sh +370 -0
  3. package/CHANGELOG.md +52 -0
  4. package/bin/n-cursor.js +30 -29
  5. package/package.json +2 -1
  6. package/rules/abie/js/applies/check.mjs +24 -0
  7. package/rules/abie/js/env_dns/check.mjs +53 -0
  8. package/rules/abie/js/firebase_hosting/check.mjs +49 -0
  9. package/rules/abie/js/hc_pairing/check.mjs +58 -0
  10. package/rules/abie/js/ua_http_route/check.mjs +86 -0
  11. package/rules/abie/js/ua_node_selector/check.mjs +65 -0
  12. package/rules/abie/policy/base_deployment_preem/target.json +10 -0
  13. package/rules/abie/policy/clean_merged_ignore_branches/target.json +4 -0
  14. package/rules/abie/policy/health_check_policy/target.json +4 -0
  15. package/rules/abie/policy/http_route_base/target.json +4 -0
  16. package/rules/abie/utils/enabled.mjs +35 -0
  17. package/rules/abie/utils/env-dns.mjs +81 -0
  18. package/rules/abie/utils/hc-yaml.mjs +27 -0
  19. package/rules/abie/utils/http-route.mjs +93 -0
  20. package/rules/abie/utils/k8s-tree.mjs +102 -0
  21. package/rules/abie/utils/kustomization-patches.mjs +224 -0
  22. package/rules/abie/utils/overlay-paths.mjs +97 -0
  23. package/rules/abie/utils/yaml.mjs +72 -0
  24. package/rules/adr/adr.mdc +82 -18
  25. package/rules/adr/js/check.mjs +84 -40
  26. package/rules/adr/policy/settings_json/settings_json.rego +17 -11
  27. package/rules/adr/policy/settings_json/target.json +4 -0
  28. package/rules/adr/policy/settings_local_json/settings_local_json.rego +24 -12
  29. package/rules/adr/policy/settings_local_json/target.json +4 -0
  30. package/rules/bun/policy/bunfig/target.json +4 -0
  31. package/rules/bun/policy/package_json/target.json +4 -0
  32. package/rules/capacitor/policy/package_json/target.json +4 -0
  33. package/rules/docker/policy/lint_docker_yml/target.json +4 -0
  34. package/rules/docker/policy/package_json/target.json +4 -0
  35. package/rules/hasura/policy/svc_hl/target.json +4 -0
  36. package/rules/image-avif/policy/package_json/target.json +4 -0
  37. package/rules/image-compress/policy/package_json/target.json +4 -0
  38. package/rules/js-bun-db/policy/package_json/target.json +4 -0
  39. package/rules/js-bun-redis/policy/package_json/target.json +4 -0
  40. package/rules/js-lint/policy/lint_js_yml/target.json +4 -0
  41. package/rules/js-lint/policy/package_json/target.json +4 -0
  42. package/rules/js-mssql/policy/package_json/target.json +4 -0
  43. package/rules/js-run/policy/configmap/target.json +4 -0
  44. package/rules/js-run/policy/package_json/target.json +4 -0
  45. package/rules/k8s/policy/base_kustomization/target.json +4 -0
  46. package/rules/k8s/policy/base_manifest/target.json +10 -0
  47. package/rules/k8s/policy/gateway/target.json +4 -0
  48. package/rules/k8s/policy/hpa_pdb/target.json +4 -0
  49. package/rules/k8s/policy/kustomization/target.json +4 -0
  50. package/rules/k8s/policy/manifest/target.json +4 -0
  51. package/rules/k8s/policy/svc_hl_yaml/target.json +4 -0
  52. package/rules/k8s/policy/svc_yaml/target.json +4 -0
  53. package/rules/npm-module/policy/emit_types_config/target.json +4 -0
  54. package/rules/npm-module/policy/npm_package_json/target.json +4 -0
  55. package/rules/npm-module/policy/npm_publish_yml/target.json +4 -0
  56. package/rules/npm-module/policy/root_package_json/target.json +4 -0
  57. package/rules/php/policy/lint_php_yml/target.json +4 -0
  58. package/rules/php/policy/package_json/target.json +4 -0
  59. package/rules/rego/js/applies/check.mjs +54 -0
  60. package/rules/rego/policy/package_json/target.json +5 -0
  61. package/rules/rego/policy/vscode_extensions/target.json +5 -0
  62. package/rules/rego/policy/vscode_settings/target.json +5 -0
  63. package/rules/style-lint/policy/lint_style_yml/target.json +4 -0
  64. package/rules/style-lint/policy/package_json/target.json +4 -0
  65. package/rules/style-lint/policy/vscode_extensions/target.json +4 -0
  66. package/rules/style-lint/policy/vscode_settings/target.json +4 -0
  67. package/rules/text/policy/cspell/target.json +4 -0
  68. package/rules/text/policy/markdownlint/target.json +4 -0
  69. package/rules/text/policy/oxfmtrc/target.json +4 -0
  70. package/rules/text/policy/package_json/target.json +4 -0
  71. package/rules/text/policy/vscode_extensions/target.json +4 -0
  72. package/rules/text/policy/vscode_settings/target.json +4 -0
  73. package/rules/vue/policy/package_json/target.json +4 -0
  74. package/schemas/target.json +58 -0
  75. package/scripts/auto-skills.mjs +2 -0
  76. package/scripts/lint-conftest.mjs +65 -414
  77. package/scripts/sync-claude-config.mjs +70 -14
  78. package/scripts/utils/discover-checkable-rules.mjs +123 -0
  79. package/scripts/utils/resolve-target-files.mjs +109 -0
  80. package/scripts/utils/run-rule.mjs +131 -0
  81. package/skills/adr-normalize/SKILL.md +71 -0
  82. package/skills/adr-normalize/auto.md +1 -0
  83. package/rules/abie/js/check.mjs +0 -1152
  84. package/rules/rego/js/check.mjs +0 -106
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Скан env-файлів abie (`*.dev.env`, `*.ua.env`): кожен внутрішньокластерний URL
3
+ * `http://<svc>.<ns>.svc.<dns>` має відповідати кластеру за іменем файла:
4
+ * - `dev.env` → `abie-dev.internal` + `dev-*` namespace
5
+ * - `ua.env` → `abie-ua.internal` + `ua-*` namespace
6
+ *
7
+ * Файл `.env` без імені (локальний для розробника) — виключено.
8
+ */
9
+ import { readFile } from 'node:fs/promises'
10
+ import { basename, 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
+
15
+ import { abieEnvNameFromBasename, collectAbieEnvFiles, validateAbieEnvInternalUrls } from '../../utils/env-dns.mjs'
16
+
17
+ /**
18
+ * @returns {Promise<number>}
19
+ */
20
+ export async function check() {
21
+ const reporter = createCheckReporter()
22
+ const { pass, fail } = reporter
23
+ const root = process.cwd()
24
+
25
+ const ignorePaths = await loadCursorIgnorePaths(root)
26
+ const envFiles = await collectAbieEnvFiles(root, ignorePaths)
27
+ if (envFiles.length === 0) {
28
+ pass('Не знайдено dev.env / ua.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
29
+ return reporter.getExitCode()
30
+ }
31
+
32
+ for (const abs of envFiles) {
33
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
34
+ const envName = abieEnvNameFromBasename(basename(abs))
35
+ if (envName === null) continue
36
+ let raw
37
+ try {
38
+ raw = await readFile(abs, 'utf8')
39
+ } catch (error) {
40
+ const msg = error instanceof Error ? error.message : String(error)
41
+ fail(`${rel}: не вдалося прочитати (${msg})`)
42
+ continue
43
+ }
44
+ const errors = validateAbieEnvInternalUrls(raw, envName)
45
+ if (errors.length === 0) {
46
+ pass(`${rel}: усі внутрішні URL відповідають env "${envName}" (abie.mdc)`)
47
+ } else {
48
+ for (const err of errors) fail(`${rel}: ${err} (abie.mdc)`)
49
+ }
50
+ }
51
+
52
+ return reporter.getExitCode()
53
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Перевірка abie: у **підкаталогах першого рівня** (без `.git`/`node_modules`) не має бути
3
+ * `.firebaserc`, `firebase.json`, `.firebase/` (abie.mdc — Firebase Hosting заборонено).
4
+ * У самому корені репозиторію ці імена не перевіряються (можуть бути від суміжних проєктів).
5
+ */
6
+ import { existsSync } from 'node:fs'
7
+ import { readdir } from 'node:fs/promises'
8
+ import { join } from 'node:path'
9
+
10
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
11
+
12
+ const SKIP_TOP_DIR_NAMES = new Set(['.git', 'node_modules'])
13
+
14
+ /**
15
+ * @returns {Promise<number>}
16
+ */
17
+ export async function check() {
18
+ const reporter = createCheckReporter()
19
+ const { pass, fail } = reporter
20
+ const root = process.cwd()
21
+
22
+ let entries
23
+ try {
24
+ entries = await readdir(root, { withFileTypes: true })
25
+ } catch (error) {
26
+ const msg = error instanceof Error ? error.message : String(error)
27
+ fail(`Не вдалося прочитати ${root} для перевірки Firebase Hosting: ${msg} (abie.mdc)`)
28
+ return reporter.getExitCode()
29
+ }
30
+ const topDirs = entries.filter(e => e.isDirectory() && !SKIP_TOP_DIR_NAMES.has(e.name))
31
+ let hasViolation = false
32
+ for (const e of topDirs) {
33
+ for (const name of ['.firebaserc', 'firebase.json']) {
34
+ const rel = join(e.name, name).replaceAll('\\', '/')
35
+ if (existsSync(join(root, e.name, name))) {
36
+ fail(`Знайдено заборонений файл Firebase Hosting: ${rel} — видали його (abie.mdc)`)
37
+ hasViolation = true
38
+ }
39
+ }
40
+ if (existsSync(join(root, e.name, '.firebase'))) {
41
+ fail(`Знайдено заборонену директорію: ${e.name}/.firebase/ — видали її (abie.mdc)`)
42
+ hasViolation = true
43
+ }
44
+ }
45
+ if (!hasViolation) {
46
+ pass('Підкаталоги кореня (1-й рівень, без .git/node_modules): артефактів Firebase Hosting не знайдено (abie.mdc)')
47
+ }
48
+ return reporter.getExitCode()
49
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Перевірка abie: для кожного каталогу з `kind: Deployment` під `k8s/` поруч має бути `hc.yaml`
3
+ * з коректним modeline (yaml-language-server $schema).
4
+ *
5
+ * Це JS-частина (FS-парність + modeline). Структурну валідацію `HealthCheckPolicy`
6
+ * (apiVersion, requestPath, port, targetRef з суфіксом `-hl`) робить CLI через
7
+ * `policy/health_check_policy/target.json` (walkGlob по hc.yaml у k8s-дереві).
8
+ */
9
+ import { existsSync } from 'node:fs'
10
+ import { readFile } from 'node:fs/promises'
11
+ import { relative } from 'node:path'
12
+
13
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
14
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
15
+
16
+ import { validateAbieHcModeline } from '../../utils/hc-yaml.mjs'
17
+ import { collectDeploymentDirs, findK8sYamlFiles } from '../../utils/k8s-tree.mjs'
18
+
19
+ /**
20
+ * @returns {Promise<number>}
21
+ */
22
+ export async function check() {
23
+ const reporter = createCheckReporter()
24
+ const { pass, fail } = reporter
25
+ const root = process.cwd()
26
+
27
+ const ignorePaths = await loadCursorIgnorePaths(root)
28
+ const yamls = await findK8sYamlFiles(root, ignorePaths)
29
+ const deploymentDirs = await collectDeploymentDirs(root, yamls, fail)
30
+
31
+ if (deploymentDirs.size === 0) {
32
+ pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
33
+ return reporter.getExitCode()
34
+ }
35
+ pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml поруч`)
36
+
37
+ for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
38
+ const hcAbs = `${dir}/hc.yaml`
39
+ const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
40
+ if (!existsSync(hcAbs)) {
41
+ fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
42
+ continue
43
+ }
44
+ let hcRaw
45
+ try {
46
+ hcRaw = await readFile(hcAbs, 'utf8')
47
+ } catch (error) {
48
+ const msg = error instanceof Error ? error.message : String(error)
49
+ fail(`${relHc}: не вдалося прочитати (${msg})`)
50
+ continue
51
+ }
52
+ const modelineErr = validateAbieHcModeline(hcRaw, relHc)
53
+ if (modelineErr !== null) fail(modelineErr)
54
+ else pass(`${relHc}: modeline OK`)
55
+ }
56
+
57
+ return reporter.getExitCode()
58
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Якщо в каталозі пакета (батько `k8s/`) є `vite.config.{js,mjs,ts}`, у `ua/kustomization.yaml`
3
+ * має бути inline-patch HTTPRoute (непорожній `target.name`): `/spec/hostnames` (домени abie),
4
+ * `/spec/parentRefs/0/namespace` (`ua` або `ua-*`).
5
+ *
6
+ * Для спільних сервісів (`auth-run-hl`, `file-link-hl`) у base-HTTPRoute пакета — кожен `backendRef`
7
+ * має `namespace: dev`; в overlay patch — JSON6902 на `/spec/rules/…/backendRefs/…/namespace` зі
8
+ * `value: ua`. Кількість patch-ів = кількість таких посилань у base.
9
+ */
10
+ import { readFile } from 'node:fs/promises'
11
+ import { relative } from 'node:path'
12
+
13
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
14
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
15
+
16
+ import { analyzeAbieSharedBackendRefsInPackageK8s } from '../../utils/http-route.mjs'
17
+ import { findK8sYamlFiles } from '../../utils/k8s-tree.mjs'
18
+ import {
19
+ getCombinedNginxRunPatchTextFromKustomization,
20
+ validateAbieNginxRunHttpRoutePatches
21
+ } from '../../utils/kustomization-patches.mjs'
22
+ import {
23
+ abiePackageDirFromK8sOverlay,
24
+ abieOverlayRequiresHttpRouteByVite,
25
+ isUaKustomizationPath
26
+ } from '../../utils/overlay-paths.mjs'
27
+
28
+ /**
29
+ * @returns {Promise<number>}
30
+ */
31
+ export async function check() {
32
+ const reporter = createCheckReporter()
33
+ const { pass, fail } = reporter
34
+ const root = process.cwd()
35
+
36
+ const ignorePaths = await loadCursorIgnorePaths(root)
37
+ const yamls = await findK8sYamlFiles(root, ignorePaths)
38
+
39
+ const uaAbsList = yamls.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
40
+ if (uaAbsList.length === 0) {
41
+ pass('Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)')
42
+ return reporter.getExitCode()
43
+ }
44
+
45
+ /** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
46
+ const cache = new Map()
47
+
48
+ for (const abs of uaAbsList) {
49
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
50
+ if (!abieOverlayRequiresHttpRouteByVite(root, abs)) {
51
+ pass(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
52
+ continue
53
+ }
54
+ const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
55
+ if (!pkgAbs) {
56
+ fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
57
+ continue
58
+ }
59
+ let p = cache.get(pkgAbs)
60
+ if (!p) {
61
+ p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamls)
62
+ cache.set(pkgAbs, p)
63
+ }
64
+ const sharedAnalysis = await p
65
+ let hasBaseError = false
66
+ for (const err of sharedAnalysis.baseErrors) {
67
+ fail(err)
68
+ hasBaseError = true
69
+ }
70
+ if (hasBaseError) continue
71
+ let raw
72
+ try {
73
+ raw = await readFile(abs, 'utf8')
74
+ } catch (error) {
75
+ const msg = error instanceof Error ? error.message : String(error)
76
+ fail(`${rel}: не вдалося прочитати (${msg})`)
77
+ continue
78
+ }
79
+ const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
80
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
81
+ if (v !== null) fail(`${rel}: ${v}`)
82
+ else pass(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
83
+ }
84
+
85
+ return reporter.getExitCode()
86
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Якщо в дереві `k8s/` пакета є `Deployment`, у `ua/kustomization.yaml` має бути inline-patch
3
+ * на `Deployment` з `path /spec/template/spec/nodeSelector` і `preem: false` (abie.mdc).
4
+ *
5
+ * Структурні обмеження JSON6902 (заборона `remove + add` на той самий path) перевіряє k8s.mdc /
6
+ * `k8s.kustomization` rego — тут лише abie-специфічне.
7
+ */
8
+ import { readFile } from 'node:fs/promises'
9
+ import { relative } from 'node:path'
10
+
11
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
12
+ import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
13
+
14
+ import { collectDeploymentDirs, findK8sYamlFiles } from '../../utils/k8s-tree.mjs'
15
+ import { kustomizationHasAbieDeploymentNodeSelectorPatch } from '../../utils/kustomization-patches.mjs'
16
+ import { abieOverlayK8sTreeHasDeployment, isUaKustomizationPath } from '../../utils/overlay-paths.mjs'
17
+
18
+ /**
19
+ * @returns {Promise<number>}
20
+ */
21
+ export async function check() {
22
+ const reporter = createCheckReporter()
23
+ const { pass, fail } = reporter
24
+ const root = process.cwd()
25
+
26
+ const ignorePaths = await loadCursorIgnorePaths(root)
27
+ const yamls = await findK8sYamlFiles(root, ignorePaths)
28
+ const deploymentDirs = await collectDeploymentDirs(root, yamls, fail)
29
+
30
+ if (deploymentDirs.size === 0) {
31
+ pass('Немає Deployment у дереві k8s — patch nodeSelector (ua) не вимагається')
32
+ return reporter.getExitCode()
33
+ }
34
+
35
+ const uaAbsList = yamls.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
36
+ if (uaAbsList.length === 0) {
37
+ fail(
38
+ 'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
39
+ )
40
+ return reporter.getExitCode()
41
+ }
42
+
43
+ for (const abs of uaAbsList) {
44
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
45
+ if (!abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
46
+ pass(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
47
+ continue
48
+ }
49
+ let raw
50
+ try {
51
+ raw = await readFile(abs, 'utf8')
52
+ } catch (error) {
53
+ const msg = error instanceof Error ? error.message : String(error)
54
+ fail(`${rel}: не вдалося прочитати (${msg})`)
55
+ continue
56
+ }
57
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
58
+ fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`)
59
+ continue
60
+ }
61
+ pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
62
+ }
63
+
64
+ return reporter.getExitCode()
65
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "walkGlob": [
5
+ "**/k8s/**/base/**/*.yaml",
6
+ "**/k8s/**/base/**/*.yml",
7
+ "!**/k8s/**/base/**/kustomization.yaml"
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": { "single": ".github/workflows/clean-merged-branch.yml" }
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": { "walkGlob": "**/k8s/**/hc.yaml" }
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": { "walkGlob": "**/k8s/**/base/**/hr.yaml" }
4
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Rule-level applies-гейт abie: чи `.n-cursor.json:rules` містить `abie`.
3
+ * Використовується `js/applies/check.mjs` як `applies()`-експорт — якщо false,
4
+ * CLI пропускає всі концерни правила (включно з policy).
5
+ */
6
+ import { existsSync } from 'node:fs'
7
+ import { readFile } from 'node:fs/promises'
8
+ import { join } from 'node:path'
9
+
10
+ const CONFIG_FILE = '.n-cursor.json'
11
+
12
+ /**
13
+ * Чи увімкнено правило **abie** у `.n-cursor.json:rules`.
14
+ * @param {string} root корінь репозиторію (cwd)
15
+ * @returns {Promise<boolean>}
16
+ */
17
+ export async function isAbieRuleEnabled(root) {
18
+ const p = join(root, CONFIG_FILE)
19
+ if (!existsSync(p)) return false
20
+ let raw
21
+ try {
22
+ raw = await readFile(p, 'utf8')
23
+ } catch {
24
+ return false
25
+ }
26
+ let cfg
27
+ try {
28
+ cfg = JSON.parse(raw)
29
+ } catch {
30
+ return false
31
+ }
32
+ const rules = cfg?.rules
33
+ if (!Array.isArray(rules)) return false
34
+ return rules.some(r => String(r).trim().toLowerCase() === 'abie')
35
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Перевірка кластерного DNS у abie env-файлах (`*.dev.env`, `*.ua.env`).
3
+ *
4
+ * abie живе у двох GKE-кластерах (`abie-dev.internal`, `abie-ua.internal`); внутрішньокластерні
5
+ * URL у env-файлі мусять відповідати кластеру за іменем файла. `validateAbieEnvInternalUrls`
6
+ * сканує всі URL виду `http://<svc>.<ns>.svc.<dns>` і вимагає коректний `<dns>` + namespace-префікс.
7
+ * Файл `.env` без імені (локальний для розробника) виключено.
8
+ */
9
+ import { basename } from 'node:path'
10
+
11
+ import { walkDir } from '../../../scripts/utils/walkDir.mjs'
12
+
13
+ const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua)\.env$/u
14
+
15
+ const ABIE_INTERNAL_URL_GLOBAL_RE =
16
+ /\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.([a-z0-9][a-z0-9-]*\.internal)(?::\d+)?(?:\/[^\s"'`]*)?/giu
17
+
18
+ const ABIE_ENV_CLUSTER_DNS_MAP = Object.freeze({
19
+ dev: Object.freeze({ clusterDns: 'abie-dev.internal', namespacePrefix: 'dev-' }),
20
+ ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
21
+ })
22
+
23
+ /**
24
+ * Дістає `dev` / `ua` з basename env-файлу abie.
25
+ * Не-abie env-файли (`production.env`, `.env` без імені) → null.
26
+ * @param {string} basenameOfEnvFile
27
+ * @returns {('dev' | 'ua') | null}
28
+ */
29
+ export function abieEnvNameFromBasename(basenameOfEnvFile) {
30
+ const m = basenameOfEnvFile.match(ABIE_ENV_FILE_BASENAME_RE)
31
+ return m ? /** @type {'dev' | 'ua'} */ (m[1]) : null
32
+ }
33
+
34
+ /**
35
+ * Сканує вміст env-файла, повертає помилки невідповідності кластерного DNS / namespace
36
+ * для кожного internal URL (один URL у двох змінних = дві окремі помилки).
37
+ * @param {string} content вміст env-файла (UTF-8)
38
+ * @param {'dev' | 'ua'} envName
39
+ * @returns {string[]} порожній масив, якщо все OK
40
+ */
41
+ export function validateAbieEnvInternalUrls(content, envName) {
42
+ const expected = ABIE_ENV_CLUSTER_DNS_MAP[envName]
43
+ if (!expected) return []
44
+ /** @type {string[]} */
45
+ const errors = []
46
+ for (const match of content.matchAll(ABIE_INTERNAL_URL_GLOBAL_RE)) {
47
+ const [fullUrl, , namespace, clusterDns] = match
48
+ if (clusterDns !== expected.clusterDns) {
49
+ errors.push(
50
+ `${fullUrl}: кластерний DNS "${clusterDns}" не відповідає env "${envName}" (очікується "${expected.clusterDns}")`
51
+ )
52
+ }
53
+ if (!namespace.startsWith(expected.namespacePrefix)) {
54
+ errors.push(
55
+ `${fullUrl}: namespace "${namespace}" не починається з "${expected.namespacePrefix}" (env "${envName}")`
56
+ )
57
+ }
58
+ }
59
+ return errors
60
+ }
61
+
62
+ /**
63
+ * Збирає `*.env` файли, які є abie env (`dev.env`/`ua.env`, опц. з провідною крапкою).
64
+ * @param {string} root корінь репозиторію
65
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів-виключень
66
+ * @returns {Promise<string[]>}
67
+ */
68
+ export async function collectAbieEnvFiles(root, ignorePaths) {
69
+ /** @type {string[]} */
70
+ const out = []
71
+ await walkDir(
72
+ root,
73
+ absPath => {
74
+ if (abieEnvNameFromBasename(basename(absPath)) !== null) {
75
+ out.push(absPath)
76
+ }
77
+ },
78
+ ignorePaths
79
+ )
80
+ return out.toSorted((a, b) => a.localeCompare(b))
81
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Валідація modeline у `hc.yaml` для abie.
3
+ * Per-document структурна валідація `HealthCheckPolicy` живе у
4
+ * `policy/health_check_policy/health_check_policy.rego` (CLI прогонить через target.json).
5
+ */
6
+ import { LINE_SPLIT_RE, MODELINE_RE, stripBom } from './yaml.mjs'
7
+
8
+ /** Очікуваний URL `$schema` для **hc.yaml** (abie.mdc). */
9
+ export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
10
+
11
+ /**
12
+ * Перевіряє modeline (`# yaml-language-server: $schema=...`) у `hc.yaml`.
13
+ * @param {string} raw вміст файла
14
+ * @param {string} relPath відносний шлях (для повідомлень)
15
+ * @returns {string | null} текст помилки або null, якщо OK
16
+ */
17
+ export function validateAbieHcModeline(raw, relPath) {
18
+ const body = stripBom(raw)
19
+ const lines = body.split(LINE_SPLIT_RE)
20
+ if (lines.length === 0 || lines[0].trim() === '') {
21
+ return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
22
+ }
23
+ const m = lines[0].match(MODELINE_RE)
24
+ if (!m) return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
25
+ if (m[1] !== ABIE_HC_SCHEMA_URL) return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
26
+ return null
27
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cross-документна аналітика abie HTTPRoute: підрахунок `backendRefs` до спільних
3
+ * сервісів (`auth-run-hl`, `file-link-hl`) у base-маніфестах пакета (поза overlay `ua`).
4
+ * Використовується ua_http_route-концерном для синхронізації числа patch-ів namespace
5
+ * у overlay із кількістю base-referencе.
6
+ */
7
+ import { relative } from 'node:path'
8
+
9
+ import { isK8sYamlInAbiePackageExcludingUaOverlay } from './overlay-paths.mjs'
10
+ import { readAndParseYamlDocs, silentFail } from './yaml.mjs'
11
+
12
+ export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'file-link-hl'])
13
+ const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NAMES)
14
+
15
+ /**
16
+ * Перевіряє один `backendRef`: якщо це спільний `-hl` сервіс, має бути `namespace: dev`.
17
+ * @param {unknown} br
18
+ * @param {string} rel rel-шлях файла
19
+ * @param {string[]} errors мутабельний список помилок
20
+ * @returns {number} 1 — це shared backend, 0 — інакше
21
+ */
22
+ function checkSharedBackendRef(br, rel, errors) {
23
+ if (br === null || typeof br !== 'object' || Array.isArray(br)) return 0
24
+ const brRec = /** @type {Record<string, unknown>} */ (br)
25
+ const name = brRec.name
26
+ if (typeof name !== 'string' || !ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) return 0
27
+ if (typeof brRec.namespace !== 'string' || brRec.namespace !== 'dev') {
28
+ errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
29
+ }
30
+ return 1
31
+ }
32
+
33
+ /**
34
+ * Збирає по HTTPRoute-документу кількість посилань на shared backends і порушення namespace.
35
+ * @param {unknown} obj корінь YAML
36
+ * @param {string} rel
37
+ * @returns {{ refCount: number, errors: string[] }}
38
+ */
39
+ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
40
+ /** @type {string[]} */
41
+ const errors = []
42
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return { refCount: 0, errors }
43
+ const rec = /** @type {Record<string, unknown>} */ (obj)
44
+ if (rec.kind !== 'HTTPRoute') return { refCount: 0, errors }
45
+ const spec = rec.spec
46
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return { refCount: 0, errors }
47
+ const rules = /** @type {Record<string, unknown>} */ (spec).rules
48
+ if (!Array.isArray(rules)) return { refCount: 0, errors }
49
+ let refCount = 0
50
+ for (const rule of rules) {
51
+ if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
52
+ const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
53
+ if (Array.isArray(brs)) {
54
+ for (const br of brs) {
55
+ refCount += checkSharedBackendRef(br, rel, errors)
56
+ }
57
+ }
58
+ }
59
+ }
60
+ return { refCount, errors }
61
+ }
62
+
63
+ /**
64
+ * Збирає по yaml-файлах пакета (поза overlay ua) кількість shared-`-hl` `backendRefs`
65
+ * і базові помилки (без `namespace: dev`).
66
+ * @param {string} root корінь репозиторію
67
+ * @param {string} pkgAbs абсолютний шлях каталогу пакета
68
+ * @param {string[]} yamlFilesAbs усі yaml під k8s
69
+ * @returns {Promise<{ refCount: number, baseErrors: string[] }>}
70
+ */
71
+ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
72
+ const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
73
+ let refCount = 0
74
+ /** @type {string[]} */
75
+ const baseErrors = []
76
+ for (const abs of yamlFilesAbs) {
77
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
78
+ if (isK8sYamlInAbiePackageExcludingUaOverlay(rel, pkgRel)) {
79
+ const docs = await readAndParseYamlDocs(abs, rel, silentFail)
80
+ if (docs) {
81
+ for (const doc of docs) {
82
+ if (doc.errors.length === 0) {
83
+ const json = doc.toJSON()
84
+ const st = httpRouteDocSharedCrossNsBackendStats(json, rel)
85
+ refCount += st.refCount
86
+ baseErrors.push(...st.errors)
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return { refCount, baseErrors }
93
+ }