@nitra/cursor 1.8.222 → 1.9.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 (33) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bin/n-cursor.js +3 -2
  3. package/mdc/abie.mdc +13 -0
  4. package/mdc/changelog.mdc +3 -2
  5. package/mdc/ci4.mdc +8 -0
  6. package/mdc/ga.mdc +3 -2
  7. package/mdc/graphql.mdc +3 -2
  8. package/mdc/hasura.mdc +3 -2
  9. package/mdc/image-avif.mdc +3 -2
  10. package/mdc/image-compress.mdc +3 -2
  11. package/mdc/k8s.mdc +1 -3
  12. package/mdc/nginx-default-tpl.mdc +3 -1
  13. package/mdc/php.mdc +3 -2
  14. package/mdc/style-lint.mdc +3 -2
  15. package/mdc/vue.mdc +3 -2
  16. package/package.json +1 -1
  17. package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
  18. package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
  19. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
  20. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
  21. package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
  22. package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
  23. package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
  24. package/policy/k8s/kustomization/kustomization.rego +2 -2
  25. package/policy/k8s/manifest/manifest.rego +4 -2
  26. package/scripts/check-abie.mjs +102 -369
  27. package/scripts/check-ga.mjs +89 -9
  28. package/scripts/check-k8s.mjs +129 -704
  29. package/scripts/lint-conftest.mjs +25 -2
  30. package/scripts/lint-ga.mjs +18 -132
  31. package/scripts/utils/run-conftest-batch.mjs +117 -0
  32. package/policy/k8s/kustomize_managed/kustomize_managed.rego +0 -31
  33. package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +0 -30
@@ -131,6 +131,7 @@ import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
131
131
 
132
132
  import { createCheckReporter } from './utils/check-reporter.mjs'
133
133
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
134
+ import { runConftestBatch } from './utils/run-conftest-batch.mjs'
134
135
  import { walkDir } from './utils/walkDir.mjs'
135
136
 
136
137
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
@@ -359,18 +360,6 @@ export function isForbiddenK8sDevPath(rel) {
359
360
  return n.includes('/k8s/dev/')
360
361
  }
361
362
 
362
- /**
363
- * Відносний шлях від кореня репозиторію у вигляді з `/` (для множини kustomize).
364
- * @param {string} root корінь cwd
365
- * @param {string} abs абсолютний шлях
366
- * @returns {string | null} posix-відносний шлях або null, якщо поза root
367
- */
368
- function posixRelFromAbs(root, abs) {
369
- const r = (relative(root, abs) || abs).replaceAll('\\', '/')
370
- if (r.startsWith('..')) return null
371
- return r
372
- }
373
-
374
363
  /**
375
364
  * Вбудовані та поширені **кластерні** `kind`, для яких **`metadata.namespace`** не застосовується.
376
365
  * CRD з невідомим kind лишаються з вимогою namespace, якщо файл не в kustomization — за потреби додай path у `resources`.
@@ -477,25 +466,9 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
477
466
  return null
478
467
  }
479
468
 
480
- /**
481
- * Усі **`kustomization.yaml`**: **`resources`**, відсортовані за en.
482
- * @param {string} root корінь репо
483
- * @param {string[]} yamlFilesAbs yaml під k8s
484
- * @param {(msg: string) => void} fail функція для фіксації порушення
485
- * @returns {Promise<void>} завершується після перевірки всіх kustomization.yaml
486
- */
487
- async function validateKustomizationResourcesSortedAlphabetically(root, yamlFilesAbs, fail) {
488
- for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
489
- const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
490
- const kust = await readFirstYamlObject(kustAbs)
491
- if (kust !== null) {
492
- const v = kustomizationResourcesSortedAlphabeticallyViolation(kust)
493
- if (v !== null) {
494
- fail(`${rel}: ${v}`)
495
- }
496
- }
497
- }
498
- }
469
+ // Plan B: per-document `resources[]` sort у Kustomization — у rego-пакеті
470
+ // `k8s.kustomization`, викликається з `runAllK8sRego` на початку `check()`.
471
+ // JS-orchestrator validateKustomizationResourcesSortedAlphabetically видалено.
499
472
 
500
473
  /**
501
474
  * Лексичне порівняння двох tuple рядків через `localeCompare('en', { sensitivity: 'base' })`.
@@ -662,42 +635,9 @@ export function kustomizationInlinePatchOpsSortedViolation(patchText) {
662
635
  return `inline patch (JSON6902) має бути за алфавітом по path. Зараз: ${paths.join(', ')}; очікувано: ${want.join(', ')} (k8s.mdc)`
663
636
  }
664
637
 
665
- /**
666
- * Усі **`kustomization.yaml`**: `patches[]` відсортовано за `[target.kind, target.name, …]`,
667
- * а вміст inline `patches[i].patch` (де всі ops — `add`/`replace` і шляхи дизʼюнктні) — за `path`.
668
- * @param {string} root корінь репо
669
- * @param {string[]} yamlFilesAbs yaml під k8s
670
- * @param {(msg: string) => void} fail функція для фіксації порушення
671
- * @returns {Promise<void>} завершується після перевірки всіх kustomization.yaml
672
- */
673
- async function validateKustomizationPatchesStructuralSort(root, yamlFilesAbs, fail) {
674
- for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
675
- const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
676
- const kust = await readFirstYamlObject(kustAbs)
677
- if (kust === null) continue
678
- const outer = kustomizationPatchesSortedViolation(kust)
679
- if (outer !== null) fail(`${rel}: ${outer}`)
680
- if (!Array.isArray(kust.patches)) continue
681
- validateInlinePatchesSorted(rel, kust.patches, fail)
682
- }
683
- }
684
-
685
- /**
686
- * Перевіряє, що inline-`patch:` (рядок YAML/JSON) у кожному `patches[i]` має ops у канонічному порядку
687
- * (`add`/`replace` за `path`). Чужі форми (без `patch`-стрічки, з `target` без inline-блока) пропускаються.
688
- * @param {string} rel відносний шлях `kustomization.yaml` для повідомлень
689
- * @param {unknown[]} patches масив `kust.patches` (рекордів)
690
- * @param {(msg: string) => void} fail callback при порушенні
691
- */
692
- function validateInlinePatchesSorted(rel, patches, fail) {
693
- for (const [i, p] of patches.entries()) {
694
- if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
695
- const rec = /** @type {Record<string, unknown>} */ (p)
696
- if (typeof rec.patch !== 'string') continue
697
- const v = kustomizationInlinePatchOpsSortedViolation(rec.patch)
698
- if (v !== null) fail(`${rel}: patches[${i}] (${kustomizationPatchLabel(p, i)}): ${v}`)
699
- }
700
- }
638
+ // Plan B: validateKustomizationPatchesStructuralSort видалено. Per-document
639
+ // `patches[]` sort + inline JSON6902 ops sort — у rego-пакеті `k8s.kustomization`,
640
+ // викликається з `runAllK8sRego`.
701
641
 
702
642
  /**
703
643
  * Шляхи з полів Kustomization для resolve відносно каталогу **`kustomization.yaml`**.
@@ -935,104 +875,6 @@ async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
935
875
  }
936
876
  }
937
877
 
938
- /**
939
- * Збирає відносні шляхи (posix) до YAML, підключених до Kustomize з будь-якого **`kustomization.yaml`** під `k8s`.
940
- * Обходить **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**;
941
- * для каталогу з **`kustomization.yaml`** виконує рекурсивний обхід.
942
- * @param {string} root корінь репозиторію
943
- * @param {string[]} yamlFilesAbs відсортовані абсолютні шляхи до `*.yaml` / `*.yml` під k8s (для `.yml` check-k8s вимагає перейменувати на `.yaml`)
944
- * @returns {Promise<Set<string>>} множина відносних шляхів до керованих файлів
945
- */
946
- export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
947
- /** @type {Set<string>} */
948
- const managed = new Set()
949
- const kustomizationAbsList = yamlFilesAbs.filter(abs => {
950
- const b = basename(abs).toLowerCase()
951
- return b === 'kustomization.yaml'
952
- })
953
-
954
- /** @type {Set<string>} */
955
- const visitedKustomization = new Set()
956
-
957
- /**
958
- * @param {string} kustAbs абсолютний шлях до kustomization.yaml
959
- * @returns {Promise<void>}
960
- */
961
- async function walkKustomization(kustAbs) {
962
- const normKust = resolve(kustAbs)
963
- if (visitedKustomization.has(normKust)) return
964
- visitedKustomization.add(normKust)
965
-
966
- let raw
967
- try {
968
- raw = await readFile(normKust, 'utf8')
969
- } catch {
970
- return
971
- }
972
- const lines = toLines(raw)
973
- const body = yamlBodyAfterModeline(lines)
974
-
975
- /** @type {import('yaml').Document[] | undefined} */
976
- let docs
977
- try {
978
- docs = parseAllDocuments(body)
979
- } catch {
980
- return
981
- }
982
- const first = docs[0]?.toJSON()
983
- if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) return
984
-
985
- const kustDir = dirname(normKust)
986
- const pathRefs = pathsFromKustomizationObject(first)
987
-
988
- /**
989
- * @param {string} ref шлях з kustomization
990
- * @returns {Promise<void>}
991
- */
992
- async function handleKustomizeManagedPathRef(ref) {
993
- if (ref.includes('://')) {
994
- return
995
- }
996
- const resolved = resolve(kustDir, ref)
997
- let st
998
- try {
999
- st = await stat(resolved)
1000
- } catch {
1001
- st = undefined
1002
- }
1003
- if (!st) {
1004
- return
1005
- }
1006
- if (st.isFile()) {
1007
- if (YAML_EXTENSION_RE.test(resolved)) {
1008
- const pr = posixRelFromAbs(root, resolved)
1009
- if (pr !== null) {
1010
- managed.add(pr)
1011
- }
1012
- }
1013
- return
1014
- }
1015
- if (!st.isDirectory()) {
1016
- return
1017
- }
1018
- const childK = existsSync(join(resolved, 'kustomization.yaml')) ? join(resolved, 'kustomization.yaml') : null
1019
- if (childK !== null) {
1020
- await walkKustomization(childK)
1021
- }
1022
- }
1023
-
1024
- for (const ref of pathRefs) {
1025
- await handleKustomizeManagedPathRef(ref)
1026
- }
1027
- }
1028
-
1029
- for (const k of kustomizationAbsList) {
1030
- await walkKustomization(k)
1031
- }
1032
-
1033
- return managed
1034
- }
1035
-
1036
878
  /**
1037
879
  * Шляхи лише з полів ресурсів Kustomization (**без** patch-файлів).
1038
880
  * @param {unknown} obj корінь першого документа Kustomization
@@ -1379,7 +1221,7 @@ async function kustomizationTreeHasDeploymentUnderK8sBase(kustAbs, rootNorm) {
1379
1221
 
1380
1222
  /**
1381
1223
  * Збирає дескриптори ресурсів з **`resources` / `bases` / `components` / `crds`** для одного дерева kustomization.
1382
- * Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок (як у **`collectKustomizeManagedRelPaths`**).
1224
+ * Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок.
1383
1225
  * @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
1384
1226
  * @param {string} rootNorm нормалізований абсолютний корінь репозиторію
1385
1227
  * @param {Set<string>} visitedKustomization нормалізовані абсолютні шляхи відвіданих **kustomization.yaml**
@@ -2285,111 +2127,11 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
2285
2127
  return out.toSorted((a, b) => a.localeCompare(b))
2286
2128
  }
2287
2129
 
2288
- /**
2289
- * Реєструє порушення, якщо в JSON6902-операціях є **remove** і **add** на один **path**.
2290
- * @param {string} rel відносний шлях до kustomization.yaml
2291
- * @param {string} label фрагмент повідомлення (наприклад `patches[1] inline JSON6902`)
2292
- * @param {string} patchText текст patch
2293
- * @param {(msg: string) => void} fail реєстрація порушення
2294
- * @returns {void}
2295
- */
2296
- function failIfJson6902RemoveAddConflictOnSamePath(rel, label, patchText, fail) {
2297
- const ops = collectJson6902OperationsFromPatchText(patchText)
2298
- const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
2299
- if (bad.length > 0) {
2300
- fail(`${rel}: ${label}: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`)
2301
- }
2302
- }
2303
-
2304
- /**
2305
- * Зовнішній patch-файл (масив JSON6902): remove+add на один path.
2306
- * @param {string} rel відносний шлях до kustomization.yaml
2307
- * @param {string} resolved абсолютний шлях до файлу patch
2308
- * @param {string} root корінь репо
2309
- * @param {string} patchRef відносне посилання з kustomization
2310
- * @param {(msg: string) => void} fail реєстрація порушення
2311
- * @returns {Promise<void>}
2312
- */
2313
- async function auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail) {
2314
- /** @type {import('node:fs').Stats | null} */
2315
- let st
2316
- try {
2317
- st = await stat(resolved)
2318
- } catch {
2319
- st = null
2320
- }
2321
- if (st === null || !st.isFile()) {
2322
- return
2323
- }
2324
- let pRaw
2325
- try {
2326
- pRaw = await readFile(resolved, 'utf8')
2327
- } catch {
2328
- return
2329
- }
2330
- const ops = collectJson6902OperationsFromPatchText(pRaw)
2331
- if (ops.length === 0) {
2332
- return
2333
- }
2334
- const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
2335
- if (bad.length === 0) {
2336
- return
2337
- }
2338
- const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
2339
- fail(
2340
- `${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
2341
- )
2342
- }
2343
-
2344
- /**
2345
- * Один елемент **`patches[]`**: inline JSON6902 або зовнішній patch-файл.
2346
- * @param {string} rel відносний шлях до kustomization.yaml
2347
- * @param {Record<string, unknown>} pr об’єкт patch
2348
- * @param {number} patchIdx 1-based індекс у масиві
2349
- * @param {string} kustAbs абсолютний шлях до kustomization.yaml
2350
- * @param {string} rootNorm нормалізований корінь репо
2351
- * @param {string} root корінь репо
2352
- * @param {(msg: string) => void} fail реєстрація порушення
2353
- * @returns {Promise<void>}
2354
- */
2355
- async function auditOneKustomizationJson6902Patch(rel, pr, patchIdx, kustAbs, rootNorm, root, fail) {
2356
- if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
2357
- failIfJson6902RemoveAddConflictOnSamePath(rel, `patches[${patchIdx}] inline JSON6902`, pr.patch, fail)
2358
- }
2359
- if (typeof pr.path !== 'string' || pr.path.trim() === '') {
2360
- return
2361
- }
2362
- const patchRef = pr.path.trim()
2363
- const resolved = resolve(dirname(kustAbs), patchRef)
2364
- if (!resolvedFilePathIsUnderRoot(rootNorm, resolved) || !existsSync(resolved)) {
2365
- return
2366
- }
2367
- await auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail)
2368
- }
2369
-
2370
- /**
2371
- * Усі **`patches[]`** у Kustomization: inline та зовнішні файли.
2372
- * @param {string} rel відносний шлях до kustomization.yaml
2373
- * @param {unknown} patches поле **patches**
2374
- * @param {string} kustAbs абсолютний шлях до kustomization.yaml
2375
- * @param {string} rootNorm нормалізований корінь репо
2376
- * @param {string} root корінь репо
2377
- * @param {(msg: string) => void} fail реєстрація порушення
2378
- * @returns {Promise<void>}
2379
- */
2380
- async function auditKustomizationPatchesJson6902(rel, patches, kustAbs, rootNorm, root, fail) {
2381
- if (!Array.isArray(patches)) {
2382
- return
2383
- }
2384
- let patchIdx = 0
2385
- for (const p of patches) {
2386
- patchIdx++
2387
- if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
2388
- const pr = /** @type {Record<string, unknown>} */ (p)
2389
- await auditOneKustomizationJson6902Patch(rel, pr, patchIdx, kustAbs, rootNorm, root, fail)
2390
- }
2391
- }
2392
- }
2130
+ // Plan B: вся audit-ланка JSON6902 (failIfJson6902RemoveAddConflictOnSamePath,
2131
+ // auditJson6902PatchExternalFile, auditOneKustomizationJson6902Patch,
2132
+ // auditKustomizationPatchesJson6902) видалена. Per-document inline JSON6902
2133
+ // remove+add conflict у rego-пакеті `k8s.kustomization`. Зовнішні patch-файли
2134
+ // не охоплені rego-кроком (потребує FS-доступу) — це trade-off Plan B.
2393
2135
 
2394
2136
  /**
2395
2137
  * Один YAML-документ: якщо це Kustomization — перевірка **patches** на JSON6902 remove+add.
@@ -2401,140 +2143,18 @@ async function auditKustomizationPatchesJson6902(rel, patches, kustAbs, rootNorm
2401
2143
  * @param {(msg: string) => void} fail реєстрація порушення
2402
2144
  * @returns {Promise<void>}
2403
2145
  */
2404
- async function auditJson6902ForKustomizationYamlDoc(rel, rootObj, kustAbs, rootNorm, root, fail) {
2405
- const rec = /** @type {Record<string, unknown>} */ (rootObj)
2406
- if (rec.kind !== 'Kustomization') {
2407
- return
2408
- }
2409
- await auditKustomizationPatchesJson6902(rel, rec.patches, kustAbs, rootNorm, root, fail)
2410
- }
2411
-
2412
- /**
2413
- * Один **`kustomization.yaml`**: JSON6902 remove+add на одному path.
2414
- * @param {string} root корінь репозиторію
2415
- * @param {string} rootNorm нормалізований корінь
2416
- * @param {string} kustAbs абсолютний шлях до файлу
2417
- * @param {(msg: string) => void} fail реєстрація порушення
2418
- * @returns {Promise<void>}
2419
- */
2420
- async function auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fail) {
2421
- const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
2422
- let raw
2423
- try {
2424
- raw = await readFile(kustAbs, 'utf8')
2425
- } catch (error) {
2426
- const msg = error instanceof Error ? error.message : String(error)
2427
- fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
2428
- return
2429
- }
2430
- const lines = toLines(raw)
2431
- const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
2432
- /** @type {import('yaml').Document[]} */
2433
- let docs
2434
- try {
2435
- docs = parseAllDocuments(body)
2436
- } catch {
2437
- return
2438
- }
2439
- for (const doc of docs) {
2440
- if (doc.errors.length === 0) {
2441
- const rootObj = doc.toJSON()
2442
- if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
2443
- await auditJson6902ForKustomizationYamlDoc(rel, rootObj, kustAbs, rootNorm, root, fail)
2444
- }
2445
- }
2446
- }
2447
- }
2448
-
2449
- /**
2450
- * Перевіряє всі **`kustomization.yaml`** під **`k8s`**: у inline **`patch`** і у зовнішніх patch-файлах не має бути **remove** і **add** на той самий **path**.
2451
- * @param {string} root корінь репозиторію
2452
- * @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
2453
- * @param {(msg: string) => void} fail реєстрація порушення
2454
- * @returns {Promise<void>}
2455
- */
2456
- async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
2457
- const rootNorm = resolve(root)
2458
- for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
2459
- await auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fail)
2460
- }
2461
- }
2462
-
2463
- /**
2464
- * Заборонений **kind: Ingress** у документі.
2465
- * @param {string} rel відносний шлях до файлу
2466
- * @param {number} docIndex 1-based індекс документа
2467
- * @param {Record<string, unknown>} rec корінь маніфесту
2468
- * @param {(msg: string) => void} fail реєстрація помилки
2469
- * @returns {void}
2470
- */
2471
- function failIfIngressInDocument(rel, docIndex, rec, fail) {
2472
- if (rec.kind !== 'Ingress') {
2473
- return
2474
- }
2475
- fail(
2476
- `${rel}: знайдено kind: Ingress (документ ${docIndex}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml) (див. k8s.mdc)`
2477
- )
2478
- }
2479
-
2480
- /**
2481
- * Чи маніфест використовує заборонений **`apiVersion: autoscaling/v1`** (HPA).
2482
- * Канон — **`autoscaling/v2`** (див. k8s.mdc).
2483
- * @param {unknown} manifest корінь YAML-документа
2484
- * @returns {boolean} true, якщо `apiVersion === 'autoscaling/v1'`
2485
- */
2486
- export function isForbiddenAutoscalingV1Manifest(manifest) {
2487
- if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
2488
- return false
2489
- const rec = /** @type {Record<string, unknown>} */ (manifest)
2490
- return rec.apiVersion === 'autoscaling/v1'
2491
- }
2492
-
2493
2146
  /**
2494
- * Заборонена група **`apiVersion: autoscaling/v1`** (HPA) вимагається міграція на **`autoscaling/v2`**.
2495
- * @param {string} rel відносний шлях до файлу
2496
- * @param {number} docIndex 1-based індекс документа
2497
- * @param {Record<string, unknown>} rec корінь маніфесту
2498
- * @param {(msg: string) => void} fail реєстрація помилки
2499
- * @returns {void}
2147
+ * Plan B: per-document JSON6902 remove+add conflict у rego-пакеті
2148
+ * `k8s.kustomization`, виклик через `runAllK8sRego`. JS-функції
2149
+ * auditJson6902ForKustomizationYamlDoc, auditJson6902OneKustomizationYamlFile,
2150
+ * validateKustomizationJson6902NoRemoveAddSamePath видалено.
2500
2151
  */
2501
- function failIfAutoscalingV1InDocument(rel, docIndex, rec, fail) {
2502
- if (!isForbiddenAutoscalingV1Manifest(rec)) {
2503
- return
2504
- }
2505
- const kind = typeof rec.kind === 'string' ? rec.kind : '(невідомо)'
2506
- fail(
2507
- `${rel}: знайдено apiVersion: autoscaling/v1 (документ ${docIndex}, kind: ${kind}) — мігруй на autoscaling/v2 (див. k8s.mdc)`
2508
- )
2509
- }
2510
2152
 
2511
- /**
2512
- * Шукає заборонені маніфести у розібраних документах: **kind: Ingress** і **apiVersion: autoscaling/v1**.
2513
- * @param {string} rel відносний шлях до файлу
2514
- * @param {string} body YAML після modeline
2515
- * @param {(msg: string) => void} fail callback для помилки
2516
- * @returns {void}
2517
- */
2518
- function scanForbiddenManifestsInYamlDocuments(rel, body, fail) {
2519
- /** @type {import('yaml').Document[]} */
2520
- let docs
2521
- try {
2522
- docs = parseAllDocuments(body)
2523
- } catch {
2524
- return
2525
- }
2526
-
2527
- for (const [di, doc] of docs.entries()) {
2528
- if (doc.errors.length === 0) {
2529
- const obj = doc.toJSON()
2530
- if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
2531
- const rec = /** @type {Record<string, unknown>} */ (obj)
2532
- failIfIngressInDocument(rel, di + 1, rec, fail)
2533
- failIfAutoscalingV1InDocument(rel, di + 1, rec, fail)
2534
- }
2535
- }
2536
- }
2537
- }
2153
+ // Plan B: per-document `kind: Ingress` і `apiVersion: autoscaling/v1` заборонено —
2154
+ // у rego-пакеті `k8s.manifest`, виклик через `runAllK8sRego`. JS-функції
2155
+ // failIfIngressInDocument, failIfAutoscalingV1InDocument, scanForbiddenManifestsInYamlDocuments
2156
+ // видалено. `isForbiddenAutoscalingV1Manifest` як публічний predicate теж видалено
2157
+ // (rego авторитативне джерело).
2538
2158
 
2539
2159
  /**
2540
2160
  * Рекомендоване **`resources.requests.cpu`** поза шарем base для підказок у повідомленнях (k8s.mdc).
@@ -3086,15 +2706,6 @@ export function serviceForbiddenGcpAnnotationsViolation(manifest) {
3086
2706
  /** Суфікс **`metadata.name`** headless-сервісу поруч із **`svc.yaml`** (див. k8s.mdc). */
3087
2707
  const SVC_HL_NAME_SUFFIX = '-hl'
3088
2708
 
3089
- /**
3090
- * Kind маршрутів Gateway API, у **`spec`** яких шукаємо **`backendRefs`** / **`backendRef`** до **Service**.
3091
- * @type {Set<string>}
3092
- */
3093
- const GATEWAY_API_ROUTE_KINDS = new Set(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute', 'UDPRoute'])
3094
-
3095
- /** Префікс **`apiVersion`** стандартних ресурсів Gateway API. */
3096
- const GATEWAY_API_GROUP_PREFIX = 'gateway.networking.k8s.io/'
3097
-
3098
2709
  /**
3099
2710
  * Чи **Service** у **`svc.yaml`** має **`spec.type: ClusterIP`** (k8s.mdc).
3100
2711
  * @param {unknown} manifest корінь YAML-документа
@@ -3281,64 +2892,9 @@ export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, ro
3281
2892
  * @param {(msg: string) => void} fail callback помилки
3282
2893
  * @returns {void}
3283
2894
  */
3284
- function failIfGatewayRouteUsesNonHeadlessService(rel, docIndex, rec, fail) {
3285
- const av = rec.apiVersion
3286
- const kind = rec.kind
3287
- if (
3288
- typeof av !== 'string' ||
3289
- !av.startsWith(GATEWAY_API_GROUP_PREFIX) ||
3290
- typeof kind !== 'string' ||
3291
- !GATEWAY_API_ROUTE_KINDS.has(kind)
3292
- ) {
3293
- return
3294
- }
3295
- const names = collectGatewayApiRouteBackendServiceNames(rec.spec)
3296
- for (const svcName of names) {
3297
- if (!svcName.endsWith(SVC_HL_NAME_SUFFIX)) {
3298
- fail(
3299
- `${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef до Service має вказувати headless-сервіс з суфіксом «${SVC_HL_NAME_SUFFIX}» у name (зараз: «${svcName}»; див. k8s.mdc)`
3300
- )
3301
- }
3302
- }
3303
- const meta = rec.metadata
3304
- if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
3305
- const routeNs = /** @type {Record<string, unknown>} */ (meta).namespace
3306
- if (typeof routeNs === 'string' && routeNs !== '') {
3307
- const redundant = collectGatewayApiRouteBackendRefsWithRedundantNamespace(rec.spec, routeNs)
3308
- for (const svcName of redundant) {
3309
- fail(
3310
- `${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef «${svcName}» має namespace «${routeNs}», що збігається з metadata.namespace маршруту — прибери поле namespace з backendRef (див. k8s.mdc)`
3311
- )
3312
- }
3313
- }
3314
- }
3315
- }
3316
-
3317
- /**
3318
- * Реєструє порушення: маршрути Gateway API мають посилатися на **Service** з суфіксом **`-hl`**.
3319
- * @param {string} rel відносний шлях до файлу
3320
- * @param {string} body YAML після modeline
3321
- * @param {(msg: string) => void} fail callback помилки
3322
- * @returns {void}
3323
- */
3324
- function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
3325
- /** @type {import('yaml').Document[]} */
3326
- let docs
3327
- try {
3328
- docs = parseAllDocuments(body)
3329
- } catch {
3330
- return
3331
- }
3332
-
3333
- for (const [di, doc] of docs.entries()) {
3334
- if (doc.errors.length === 0) {
3335
- const obj = doc.toJSON()
3336
- if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
3337
- failIfGatewayRouteUsesNonHeadlessService(rel, di + 1, /** @type {Record<string, unknown>} */ (obj), fail)
3338
- }
3339
- }
3340
- }
3341
- }
2895
+ // Plan B: Gateway API маршрут backendRef з суфіксом `-hl` і redundant namespace —
2896
+ // у rego-пакеті `k8s.gateway`, виклик через `runAllK8sRego`. JS-функції
2897
+ // failIfGatewayRouteUsesNonHeadlessService, scanGatewayApiRouteBackendRefsInYamlBody видалено.
3342
2898
 
3343
2899
  /**
3344
2900
  * Звузити `unknown` до `Record<string, unknown>` (`null`, масиви, примітиви → null).
@@ -3815,36 +3371,28 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
3815
3371
  const { hasuraByDir, httpRoutes } = await collectHasuraDeploymentsAndHttpRoutes(yamlFiles)
3816
3372
  if (hasuraByDir.size === 0 || httpRoutes.length === 0) return
3817
3373
 
3374
+ // JS gating: відберемо файли HTTPRoute, що paired з Hasura-Deployment у тому ж каталозі
3375
+ // (за `metadata.name` HTTPRoute === metadata.name Hasura-Deployment). Per-document валідація
3376
+ // канону 4 правил Hasura — у rego-пакеті `k8s.hasura_httproute`.
3377
+ const pairedFiles = new Set()
3818
3378
  for (const hr of httpRoutes) {
3819
3379
  const meta = asPlainRecord(hr.obj.metadata)
3820
3380
  const name = meta === null ? undefined : meta.name
3821
3381
  const set = typeof name === 'string' && name !== '' ? hasuraByDir.get(hr.dir) : undefined
3822
3382
  if (set !== undefined && typeof name === 'string' && set.has(name)) {
3823
- const v = httpRouteHasuraCanonViolation(hr.obj)
3824
- if (v !== null) {
3825
- const rel = (relative(root, hr.abs) || hr.abs).replaceAll('\\', '/')
3826
- fail(
3827
- `${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`
3828
- )
3829
- }
3383
+ pairedFiles.add(hr.abs)
3830
3384
  }
3831
3385
  }
3832
- }
3833
-
3834
- /**
3835
- * Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
3836
- * @param {unknown} manifest корінь YAML-документа
3837
- * @returns {string | null} текст порушення або null, якщо поля немає
3838
- */
3839
- export function metadataNamespaceForbiddenViolation(manifest) {
3840
- if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
3841
- return null
3842
- const rec = /** @type {Record<string, unknown>} */ (manifest)
3843
- const meta = rec.metadata
3844
- if (meta !== null && typeof meta === 'object' && !Array.isArray(meta) && 'namespace' in meta) {
3845
- return 'metadata.namespace заборонено — namespace задає kustomization.yaml (поле namespace); файл підключено через resources / patches / … (див. k8s.mdc)'
3386
+ if (pairedFiles.size === 0) return
3387
+ const violations = runConftestBatch({
3388
+ policyDirRel: 'k8s/hasura_httproute',
3389
+ namespace: 'k8s.hasura_httproute',
3390
+ files: [...pairedFiles]
3391
+ })
3392
+ for (const v of violations) {
3393
+ const rel = (relative(root, v.filename) || v.filename).replaceAll('\\', '/')
3394
+ fail(`${rel}: ${v.message}`)
3846
3395
  }
3847
- return null
3848
3396
  }
3849
3397
 
3850
3398
  /**
@@ -3898,117 +3446,11 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
3898
3446
  return K8S_BASE_SEGMENT_RE.test(n)
3899
3447
  }
3900
3448
 
3901
- /**
3902
- * Правила **metadata.namespace** для одного документа.
3903
- * @param {string} rel відносний шлях
3904
- * @param {number} docIndex 1-based
3905
- * @param {unknown} obj корінь документа
3906
- * @param {boolean} skipMetaNs пропуск для **kustomization.yaml**
3907
- * @param {boolean} inBaseManifest файл у **k8s/base/**
3908
- * @param {boolean} kustomizeManaged файл у графі kustomization
3909
- * @param {(msg: string) => void} fail реєстрація помилки
3910
- * @returns {void}
3911
- */
3912
- function failIfK8sPolicyNamespaceRulesViolated(rel, docIndex, obj, skipMetaNs, inBaseManifest, kustomizeManaged, fail) {
3913
- if (skipMetaNs) {
3914
- return
3915
- }
3916
- if (inBaseManifest) {
3917
- const req = metadataNamespaceRequiredViolation(obj, true)
3918
- if (req !== null) {
3919
- fail(`${rel}: документ ${docIndex}: ${req}`)
3920
- }
3921
- return
3922
- }
3923
- if (kustomizeManaged) {
3924
- const ns = metadataNamespaceForbiddenViolation(obj)
3925
- if (ns !== null) {
3926
- fail(`${rel}: документ ${docIndex}: ${ns}`)
3927
- }
3928
- return
3929
- }
3930
- const req = metadataNamespaceRequiredViolation(obj, false)
3931
- if (req !== null) {
3932
- fail(`${rel}: документ ${docIndex}: ${req}`)
3933
- }
3934
- }
3935
-
3936
- /**
3937
- * Deployment / Service / HealthCheckPolicy — політики для одного документа.
3938
- * @param {string} rel відносний шлях
3939
- * @param {string} baseLower basename (нижній регістр)
3940
- * @param {number} docIndex 1-based
3941
- * @param {unknown} obj корінь документа
3942
- * @param {(msg: string) => void} fail реєстрація помилки
3943
- * @returns {void}
3944
- */
3945
- function failIfK8sPolicyResourceRulesViolated(rel, baseLower, docIndex, obj, fail) {
3946
- const relPosix = rel.replaceAll('\\', '/')
3947
- const inK8sBaseLayer = isK8sYamlUnderBaseDirectory(relPosix)
3948
- const resV = deploymentResourcesViolation(obj, inK8sBaseLayer)
3949
- if (resV !== null) {
3950
- fail(`${rel}: Deployment (документ ${docIndex}): ${resV}`)
3951
- }
3952
- const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
3953
- if (hasuraV !== null) {
3954
- fail(`${rel}: Deployment (документ ${docIndex}): ${hasuraV}`)
3955
- }
3956
- const svcGcpV = serviceForbiddenGcpAnnotationsViolation(obj)
3957
- if (svcGcpV !== null) {
3958
- fail(`${rel}: Service (документ ${docIndex}): ${svcGcpV}`)
3959
- }
3960
- if (baseLower === 'svc.yaml') {
3961
- const svcT = serviceSvcYamlClusterIpTypeViolation(obj)
3962
- if (svcT !== null) {
3963
- fail(`${rel}: Service (документ ${docIndex}): ${svcT}`)
3964
- }
3965
- }
3966
- if (baseLower === 'svc-hl.yaml') {
3967
- const svcH = serviceSvcHlYamlHeadlessViolation(obj)
3968
- if (svcH !== null) {
3969
- fail(`${rel}: Service (документ ${docIndex}): ${svcH}`)
3970
- }
3971
- }
3972
- const hcpHl = healthCheckPolicyTargetRefHeadlessServiceViolation(obj)
3973
- if (hcpHl !== null) {
3974
- fail(`${rel}: документ ${docIndex}: ${hcpHl}`)
3975
- }
3976
- }
3977
-
3978
- /**
3979
- * Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
3980
- * **Service — заборонені GKE-анотації**, **`svc.yaml`** (**`spec.type: ClusterIP`**), **`svc-hl.yaml`**
3981
- * (**headless**, суфікс **`-hl`** у **`metadata.name`**), **HealthCheckPolicy** (**`targetRef.name`** з **`-hl`**).
3982
- * @param {string} rel відносний шлях
3983
- * @param {string} baseLower basename файлу (нижній регістр)
3984
- * @param {string} body вміст після modeline
3985
- * @param {(msg: string) => void} fail реєстрація помилки
3986
- * @param {boolean} kustomizeManaged чи файл досяжний з kustomization.yaml (resources / patches / …)
3987
- */
3988
- function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged) {
3989
- /** @type {import('yaml').Document[]} */
3990
- let docs
3991
- try {
3992
- docs = parseAllDocuments(body)
3993
- } catch (error) {
3994
- const msg = error instanceof Error ? error.message : String(error)
3995
- fail(`${rel}: не вдалося розібрати YAML для перевірок маніфестів (${msg})`)
3996
- return
3997
- }
3998
-
3999
- const skipMetaNs = isKustomizationFileName(baseLower)
4000
- const inBaseManifest = isK8sBaseManifestYamlPath(rel, baseLower)
4001
-
4002
- for (const [di, doc] of docs.entries()) {
4003
- if (doc.errors.length > 0) {
4004
- fail(`${rel}: YAML (документ ${di + 1}): ${doc.errors.map(e => e.message).join('; ')}`)
4005
- } else {
4006
- const obj = doc.toJSON()
4007
- failIfK8sPolicyNamespaceRulesViolated(rel, di + 1, obj, skipMetaNs, inBaseManifest, kustomizeManaged, fail)
4008
- failIfK8sPolicyResourceRulesViolated(rel, baseLower, di + 1, obj, fail)
4009
- }
4010
- }
4011
- }
3449
+ // Plan B: per-document валідаційне ядро для k8s YAML повністю в rego —
3450
+ // `k8s.manifest`, `k8s.gateway`, `k8s.svc_yaml`, `k8s.svc_hl_yaml`,
3451
+ // `k8s.base_manifest`. Виклик через `runAllK8sRego`.
3452
+ // JS-функції failIfK8sPolicyNamespaceRulesViolated, failIfK8sPolicyResourceRulesViolated,
3453
+ // validateK8sYamlPolicyDocuments видалено.
4012
3454
 
4013
3455
  /**
4014
3456
  * Kind для імен файлів yannh/datree: лише літери та цифри, нижній регістр (Service → service, HTTPRoute → httproute).
@@ -4103,20 +3545,6 @@ function countSchemaModelines(lines) {
4103
3545
  return lines.filter(l => OXLINT_SCHEMA_MODELINE_RE.test(l.trim())).length
4104
3546
  }
4105
3547
 
4106
- /**
4107
- * Політики маніфестів і Gateway backendRefs після розбору тіла.
4108
- * @param {string} rel відносний шлях
4109
- * @param {string} baseLower basename (нижній регістр)
4110
- * @param {string} body YAML після modeline
4111
- * @param {(msg: string) => void} fail реєстрація помилки
4112
- * @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
4113
- * @returns {void}
4114
- */
4115
- function runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel) {
4116
- const kustomizeManaged = kustomizeManagedRel.has(rel)
4117
- validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
4118
- scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
4119
- }
4120
3548
 
4121
3549
  /**
4122
3550
  * Файл з першим документом **HttpBackendGroup** (ALB Yandex): без modeline **$schema**.
@@ -4125,14 +3553,13 @@ function runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeMa
4125
3553
  * @param {string[]} lines рядки файлу
4126
3554
  * @param {(msg: string) => void} fail реєстрація помилки
4127
3555
  * @param {(msg: string) => void} pass реєстрація успіху
4128
- * @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
4129
3556
  * @returns {void}
4130
3557
  */
4131
- function checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass, kustomizeManagedRel) {
4132
- const body = lines.join('\n')
4133
- scanForbiddenManifestsInYamlDocuments(rel, body, fail)
3558
+ function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass) {
3559
+ // Per-document валідація (Ingress/autoscaling/v1 заборонено, Gateway API backendRef,
3560
+ // metadata.namespace правила) — у rego (`k8s.manifest`, `k8s.gateway`, `k8s.base_manifest`),
3561
+ // батч-виклик з `runAllK8sRego` на початку `check()`.
4134
3562
  pass(`${rel}: HttpBackendGroup (alb.yc.io/v1alpha1) — modeline $schema не застосовується (k8s.mdc)`)
4135
- runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel)
4136
3563
  }
4137
3564
 
4138
3565
  /**
@@ -4143,10 +3570,9 @@ function checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass, kus
4143
3570
  * @param {string[]} lines рядки файлу
4144
3571
  * @param {(msg: string) => void} fail реєстрація помилки
4145
3572
  * @param {(msg: string) => void} pass реєстрація успіху
4146
- * @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
4147
3573
  * @returns {void}
4148
3574
  */
4149
- function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass, kustomizeManagedRel) {
3575
+ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass) {
4150
3576
  const match = lines[0].match(MODELINE_RE)
4151
3577
  if (!match) {
4152
3578
  fail(`${rel}: некоректний modeline $schema у першому рядку`)
@@ -4160,7 +3586,9 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
4160
3586
 
4161
3587
  const body = yamlBodyAfterModeline(lines)
4162
3588
 
4163
- scanForbiddenManifestsInYamlDocuments(rel, body, fail)
3589
+ // Per-document валідація (Ingress/autoscaling/v1 заборонено, Gateway API backendRef,
3590
+ // metadata.namespace правила, Service GCP-анотації, Deployment resources/Hasura image,
3591
+ // topologySpread, HCP, svc/svc-hl) — делегована rego, виконано у `runAllK8sRego` вище.
4164
3592
 
4165
3593
  if (schemaUrl.startsWith('file:')) {
4166
3594
  pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
@@ -4181,10 +3609,7 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
4181
3609
  pass(`${rel}: $schema узгоджено (${reason})`)
4182
3610
  } else {
4183
3611
  fail(`${rel}: $schema має бути https URL або file: (див. k8s.mdc)`)
4184
- return
4185
3612
  }
4186
-
4187
- runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel)
4188
3613
  }
4189
3614
 
4190
3615
  /**
@@ -4193,10 +3618,9 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
4193
3618
  * @param {string} root корінь репозиторію
4194
3619
  * @param {(msg: string) => void} fail реєстрація помилки
4195
3620
  * @param {(msg: string) => void} pass реєстрація успіху
4196
- * @param {Set<string>} kustomizeManagedRel відносні posix-шляхи з collectKustomizeManagedRelPaths
4197
3621
  * @returns {Promise<void>}
4198
3622
  */
4199
- async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
3623
+ async function checkK8sYamlFile(abs, root, fail, pass) {
4200
3624
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
4201
3625
  const base = basename(abs)
4202
3626
  const baseLower = base.toLowerCase()
@@ -4237,7 +3661,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
4237
3661
  )
4238
3662
  return
4239
3663
  }
4240
- checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass, kustomizeManagedRel)
3664
+ checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass)
4241
3665
  return
4242
3666
  }
4243
3667
 
@@ -4246,7 +3670,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
4246
3670
  return
4247
3671
  }
4248
3672
 
4249
- checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass, kustomizeManagedRel)
3673
+ checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass)
4250
3674
  }
4251
3675
 
4252
3676
  /**
@@ -4272,46 +3696,9 @@ function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
4272
3696
  * @param {(msg: string) => void} fail реєстрація порушення
4273
3697
  * @returns {Promise<void>}
4274
3698
  */
4275
- async function verifyBaseKustomizationNamespaceOnFile(root, abs, fail) {
4276
- const rel = relative(root, abs).replaceAll('\\', '/')
4277
- try {
4278
- const raw = await readFile(abs, 'utf8')
4279
- const lines = toLines(raw)
4280
- const body = yamlBodyAfterModeline(lines)
4281
- /** @type {import('yaml').Document[] | undefined} */
4282
- let docs
4283
- try {
4284
- docs = parseAllDocuments(body)
4285
- } catch {
4286
- fail(`${rel}: не вдалося розпарсити YAML для перевірки namespace у base (див. k8s.mdc)`)
4287
- return
4288
- }
4289
- const first = docs[0]?.toJSON()
4290
- const v = baseKustomizationNamespaceViolation(first)
4291
- if (v) {
4292
- fail(`${rel}: ${v}`)
4293
- }
4294
- } catch (error) {
4295
- const msg = error instanceof Error ? error.message : String(error)
4296
- fail(`${rel}: не вдалося прочитати (${msg})`)
4297
- }
4298
- }
4299
-
4300
- /**
4301
- * Якщо є **`k8s/base/kustomization.yaml`**, у ньому **завжди** має бути непорожній **`namespace:`**.
4302
- * @param {string} root корінь репозиторію
4303
- * @param {string[]} yamlFiles абсолютні шляхи
4304
- * @param {(msg: string) => void} fail callback для реєстрації порушення
4305
- * @returns {Promise<void>}
4306
- */
4307
- async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
4308
- for (const abs of yamlFiles) {
4309
- const rel = relative(root, abs).replaceAll('\\', '/')
4310
- if (isBaseKustomizationPath(rel)) {
4311
- await verifyBaseKustomizationNamespaceOnFile(root, abs, fail)
4312
- }
4313
- }
4314
- }
3699
+ // Plan B: per-document `k8s/base/kustomization.yaml` має непорожнє поле `namespace:` —
3700
+ // у rego-пакеті `k8s.base_kustomization`, виклик через `runAllK8sRego`.
3701
+ // JS-функції verifyBaseKustomizationNamespaceOnFile, ensureBaseKustomizationHasNamespace видалено.
4315
3702
 
4316
3703
  const CONFIGMAP_BASE_PATH_RE = /\/k8s\/base\/configmap\.yaml$/u
4317
3704
 
@@ -4374,19 +3761,6 @@ async function validateConfigMapNameMatchesDeployment(root, yamlFilesAbs, fail,
4374
3761
  }
4375
3762
  }
4376
3763
 
4377
- /**
4378
- * Знаходить перший документ **ConfigMap** у файлі (з `metadata.name`).
4379
- * @param {string} absPath абсолютний шлях до YAML-файлу
4380
- * @returns {Promise<Record<string, unknown> | null>} об'єкт ConfigMap або null
4381
- */
4382
- async function readFirstConfigMapDoc(absPath) {
4383
- const raw = await tryReadFileUtf8(absPath)
4384
- if (raw === undefined) return null
4385
- const docs = tryParseAllYamlDocs(raw)
4386
- if (docs === undefined) return null
4387
- return findFirstDocByKind(docs, 'ConfigMap')
4388
- }
4389
-
4390
3764
  /**
4391
3765
  * Для кожного `k8s/base/configmap.yaml`, у каталозі якого поруч є Hasura-Deployment,
4392
3766
  * вимагає у `data` ключ **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`** (k8s.mdc).
@@ -4400,21 +3774,29 @@ async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs
4400
3774
  const rel = relative(root, abs).replaceAll('\\', '/')
4401
3775
  return CONFIGMAP_BASE_PATH_RE.test(`/${rel}`) || rel === 'k8s/base/configmap.yaml'
4402
3776
  })
3777
+ // JS gating: відберемо ConfigMap-файли, у каталозі яких поруч є Hasura-Deployment.
3778
+ // Per-document валідація `data.HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS == "true"`
3779
+ // — у rego-пакеті `k8s.hasura_configmap`.
3780
+ const paired = []
4403
3781
  for (const cmAbs of cmFiles) {
4404
- const rel = relative(root, cmAbs).replaceAll('\\', '/') || cmAbs
4405
3782
  const deployment = await findDeploymentDocInDir(dirname(cmAbs))
4406
3783
  if (deployment !== null && isHasuraDeploymentManifest(deployment)) {
4407
- const cm = await readFirstConfigMapDoc(cmAbs)
4408
- if (cm !== null) {
4409
- const violation = hasuraConfigMapRemoteSchemaPermissionsViolation(cm)
4410
- if (violation === null) {
4411
- passFn(`${rel}: ${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}="true" для Hasura-Deployment (k8s.mdc)`)
4412
- } else {
4413
- fail(`${rel}: ${violation}`)
4414
- }
4415
- }
3784
+ paired.push(cmAbs)
4416
3785
  }
4417
3786
  }
3787
+ if (paired.length === 0) return
3788
+ const violations = runConftestBatch({
3789
+ policyDirRel: 'k8s/hasura_configmap',
3790
+ namespace: 'k8s.hasura_configmap',
3791
+ files: paired
3792
+ })
3793
+ for (const v of violations) {
3794
+ const rel = (relative(root, v.filename) || v.filename).replaceAll('\\', '/')
3795
+ fail(`${rel}: ${v.message}`)
3796
+ }
3797
+ if (violations.length === 0) {
3798
+ passFn(`Hasura-ConfigMap (${paired.length}) відповідає ${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}="true" (rego)`)
3799
+ }
4418
3800
  }
4419
3801
 
4420
3802
  /**
@@ -6518,6 +5900,53 @@ async function runKustomizationImagesCleanup(kustAbs, rel, fail, pass) {
6518
5900
  * Перевіряє відповідність проєкту правилам k8s.mdc.
6519
5901
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
6520
5902
  */
5903
+ /**
5904
+ * Plan B (rego-authoritative): на початку `check()` батч-викликаємо path-фільтровані
5905
+ * rego-пакети з `npm/policy/k8s/` через `runConftestBatch`. Пакети hasura_configmap і
5906
+ * hasura_httproute мають cross-file gating (паруються з Hasura-Deployment) — вони запускаються
5907
+ * з відповідних orchestrator-функцій (`validateHasuraConfigMapRemoteSchemaPermissions`,
5908
+ * `validateHasuraHttpRouteCanon`). Структурна частина HPA/PDB (`k8s.hpa_pdb`) тут на всіх yaml,
5909
+ * env-залежні межі min/maxReplicas і expected-name — JS-cross-file у `validateDeploymentHpaPdbAndTopology`.
5910
+ * @param {string} root корінь репозиторію (cwd)
5911
+ * @param {string[]} yamlFiles абсолютні шляхи знайдених *.yaml під `…/k8s/`
5912
+ * @param {(msg: string) => void} fail callback при помилці
5913
+ * @returns {void}
5914
+ */
5915
+ function runAllK8sRego(root, yamlFiles, fail) {
5916
+ const relOf = abs => relative(root, abs).replaceAll('\\', '/') || abs
5917
+
5918
+ const allYaml = yamlFiles
5919
+ const kustYaml = yamlFiles.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')
5920
+ const svcYaml = yamlFiles.filter(p => basename(p) === 'svc.yaml')
5921
+ const svcHlYaml = yamlFiles.filter(p => basename(p) === 'svc-hl.yaml')
5922
+ const baseKustYaml = yamlFiles.filter(p => isBaseKustomizationPath(relOf(p)))
5923
+ const baseResourceYaml = yamlFiles.filter(p => {
5924
+ const r = relOf(p)
5925
+ if (!K8S_BASE_SEGMENT_RE.test(r)) return false
5926
+ return basename(p).toLowerCase() !== 'kustomization.yaml'
5927
+ })
5928
+
5929
+ /** @type {Array<{ ns: string, dir: string, files: string[] }>} */
5930
+ const targets = [
5931
+ { ns: 'k8s.manifest', dir: 'k8s/manifest', files: allYaml },
5932
+ { ns: 'k8s.gateway', dir: 'k8s/gateway', files: allYaml },
5933
+ { ns: 'k8s.hpa_pdb', dir: 'k8s/hpa_pdb', files: allYaml },
5934
+ { ns: 'k8s.kustomization', dir: 'k8s/kustomization', files: kustYaml },
5935
+ { ns: 'k8s.svc_yaml', dir: 'k8s/svc_yaml', files: svcYaml },
5936
+ { ns: 'k8s.svc_hl_yaml', dir: 'k8s/svc_hl_yaml', files: svcHlYaml },
5937
+ { ns: 'k8s.base_kustomization', dir: 'k8s/base_kustomization', files: baseKustYaml },
5938
+ { ns: 'k8s.base_manifest', dir: 'k8s/base_manifest', files: baseResourceYaml }
5939
+ ]
5940
+
5941
+ for (const t of targets) {
5942
+ if (t.files.length === 0) continue
5943
+ const violations = runConftestBatch({ policyDirRel: t.dir, namespace: t.ns, files: t.files })
5944
+ for (const v of violations) {
5945
+ fail(`${relOf(v.filename)}: ${v.message}`)
5946
+ }
5947
+ }
5948
+ }
5949
+
6521
5950
  export async function check() {
6522
5951
  const reporter = createCheckReporter()
6523
5952
  const { pass, fail } = reporter
@@ -6542,10 +5971,14 @@ export async function check() {
6542
5971
 
6543
5972
  assertNoForbiddenK8sDevPaths(yamlFiles, root, fail)
6544
5973
 
6545
- const kustomizeManagedRel = await collectKustomizeManagedRelPaths(root, yamlFiles)
5974
+ // Plan B: пер-документні структурні правила — у rego-полісі `npm/policy/k8s/*`,
5975
+ // викликаємо одним батчем на namespace через runConftestBatch. JS нижче робить
5976
+ // лише cross-file orchestration, modeline та FS-existence перевірки.
5977
+ runAllK8sRego(root, yamlFiles, fail)
5978
+ pass(`Rego-полісі (npm/policy/k8s/*) виконано на ${yamlFiles.length} файл(ах)`)
6546
5979
 
6547
5980
  for (const abs of yamlFiles) {
6548
- await checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel)
5981
+ await checkK8sYamlFile(abs, root, fail, pass)
6549
5982
  }
6550
5983
 
6551
5984
  await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
@@ -6554,20 +5987,12 @@ export async function check() {
6554
5987
 
6555
5988
  await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
6556
5989
 
6557
- await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
6558
-
6559
5990
  await validateKustomizationPathRefsExistOnDisk(root, yamlFiles, fail)
6560
5991
 
6561
- await validateKustomizationResourcesSortedAlphabetically(root, yamlFiles, fail)
6562
-
6563
- await validateKustomizationPatchesStructuralSort(root, yamlFiles, fail)
6564
-
6565
5992
  await validateKustomizationPatchTargetsResolved(root, yamlFiles, fail)
6566
5993
 
6567
5994
  await validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFiles, fail, pass)
6568
5995
 
6569
- await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
6570
-
6571
5996
  await validateConfigMapNameMatchesDeployment(root, yamlFiles, fail, pass)
6572
5997
 
6573
5998
  await validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFiles, fail, pass)