@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.
- package/.claude-template/hooks/capture-decisions.sh +3 -3
- package/.claude-template/hooks/normalize-decisions.sh +370 -0
- package/CHANGELOG.md +52 -0
- package/bin/n-cursor.js +30 -29
- package/package.json +2 -1
- package/rules/abie/js/applies/check.mjs +24 -0
- package/rules/abie/js/env_dns/check.mjs +53 -0
- package/rules/abie/js/firebase_hosting/check.mjs +49 -0
- package/rules/abie/js/hc_pairing/check.mjs +58 -0
- package/rules/abie/js/ua_http_route/check.mjs +86 -0
- package/rules/abie/js/ua_node_selector/check.mjs +65 -0
- package/rules/abie/policy/base_deployment_preem/target.json +10 -0
- package/rules/abie/policy/clean_merged_ignore_branches/target.json +4 -0
- package/rules/abie/policy/health_check_policy/target.json +4 -0
- package/rules/abie/policy/http_route_base/target.json +4 -0
- package/rules/abie/utils/enabled.mjs +35 -0
- package/rules/abie/utils/env-dns.mjs +81 -0
- package/rules/abie/utils/hc-yaml.mjs +27 -0
- package/rules/abie/utils/http-route.mjs +93 -0
- package/rules/abie/utils/k8s-tree.mjs +102 -0
- package/rules/abie/utils/kustomization-patches.mjs +224 -0
- package/rules/abie/utils/overlay-paths.mjs +97 -0
- package/rules/abie/utils/yaml.mjs +72 -0
- package/rules/adr/adr.mdc +82 -18
- package/rules/adr/js/check.mjs +84 -40
- package/rules/adr/policy/settings_json/settings_json.rego +17 -11
- package/rules/adr/policy/settings_json/target.json +4 -0
- package/rules/adr/policy/settings_local_json/settings_local_json.rego +24 -12
- package/rules/adr/policy/settings_local_json/target.json +4 -0
- package/rules/bun/policy/bunfig/target.json +4 -0
- package/rules/bun/policy/package_json/target.json +4 -0
- package/rules/capacitor/policy/package_json/target.json +4 -0
- package/rules/docker/policy/lint_docker_yml/target.json +4 -0
- package/rules/docker/policy/package_json/target.json +4 -0
- package/rules/hasura/policy/svc_hl/target.json +4 -0
- package/rules/image-avif/policy/package_json/target.json +4 -0
- package/rules/image-compress/policy/package_json/target.json +4 -0
- package/rules/js-bun-db/policy/package_json/target.json +4 -0
- package/rules/js-bun-redis/policy/package_json/target.json +4 -0
- package/rules/js-lint/policy/lint_js_yml/target.json +4 -0
- package/rules/js-lint/policy/package_json/target.json +4 -0
- package/rules/js-mssql/policy/package_json/target.json +4 -0
- package/rules/js-run/policy/configmap/target.json +4 -0
- package/rules/js-run/policy/package_json/target.json +4 -0
- package/rules/k8s/policy/base_kustomization/target.json +4 -0
- package/rules/k8s/policy/base_manifest/target.json +10 -0
- package/rules/k8s/policy/gateway/target.json +4 -0
- package/rules/k8s/policy/hpa_pdb/target.json +4 -0
- package/rules/k8s/policy/kustomization/target.json +4 -0
- package/rules/k8s/policy/manifest/target.json +4 -0
- package/rules/k8s/policy/svc_hl_yaml/target.json +4 -0
- package/rules/k8s/policy/svc_yaml/target.json +4 -0
- package/rules/npm-module/policy/emit_types_config/target.json +4 -0
- package/rules/npm-module/policy/npm_package_json/target.json +4 -0
- package/rules/npm-module/policy/npm_publish_yml/target.json +4 -0
- package/rules/npm-module/policy/root_package_json/target.json +4 -0
- package/rules/php/policy/lint_php_yml/target.json +4 -0
- package/rules/php/policy/package_json/target.json +4 -0
- package/rules/rego/js/applies/check.mjs +54 -0
- package/rules/rego/policy/package_json/target.json +5 -0
- package/rules/rego/policy/vscode_extensions/target.json +5 -0
- package/rules/rego/policy/vscode_settings/target.json +5 -0
- package/rules/style-lint/policy/lint_style_yml/target.json +4 -0
- package/rules/style-lint/policy/package_json/target.json +4 -0
- package/rules/style-lint/policy/vscode_extensions/target.json +4 -0
- package/rules/style-lint/policy/vscode_settings/target.json +4 -0
- package/rules/text/policy/cspell/target.json +4 -0
- package/rules/text/policy/markdownlint/target.json +4 -0
- package/rules/text/policy/oxfmtrc/target.json +4 -0
- package/rules/text/policy/package_json/target.json +4 -0
- package/rules/text/policy/vscode_extensions/target.json +4 -0
- package/rules/text/policy/vscode_settings/target.json +4 -0
- package/rules/vue/policy/package_json/target.json +4 -0
- package/schemas/target.json +58 -0
- package/scripts/auto-skills.mjs +2 -0
- package/scripts/lint-conftest.mjs +65 -414
- package/scripts/sync-claude-config.mjs +70 -14
- package/scripts/utils/discover-checkable-rules.mjs +123 -0
- package/scripts/utils/resolve-target-files.mjs +109 -0
- package/scripts/utils/run-rule.mjs +131 -0
- package/skills/adr-normalize/SKILL.md +71 -0
- package/skills/adr-normalize/auto.md +1 -0
- package/rules/abie/js/check.mjs +0 -1152
- 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,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
|
+
}
|