@nitra/cursor 1.8.221 → 1.8.228

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 (44) hide show
  1. package/.claude-template/npm-CLAUDE.md +4 -0
  2. package/CHANGELOG.md +69 -0
  3. package/bin/auto-rules.md +2 -0
  4. package/bin/n-cursor.js +3 -2
  5. package/mdc/abie.mdc +13 -0
  6. package/mdc/ci4.mdc +8 -0
  7. package/mdc/tauri.mdc +20 -0
  8. package/package.json +1 -1
  9. package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
  10. package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
  11. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
  12. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
  13. package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
  14. package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
  15. package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
  16. package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
  17. package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
  18. package/policy/k8s/base_manifest/base_manifest.rego +154 -0
  19. package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
  20. package/policy/k8s/gateway/gateway.rego +151 -0
  21. package/policy/k8s/gateway/gateway_test.rego +122 -0
  22. package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
  23. package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
  24. package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
  25. package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
  26. package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
  27. package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
  28. package/policy/k8s/kustomization/kustomization.rego +220 -0
  29. package/policy/k8s/kustomization/kustomization_test.rego +128 -0
  30. package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
  31. package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
  32. package/policy/k8s/manifest/manifest.rego +151 -4
  33. package/policy/k8s/manifest/manifest_test.rego +309 -0
  34. package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
  35. package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
  36. package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
  37. package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
  38. package/scripts/check-abie.mjs +102 -369
  39. package/scripts/check-ga.mjs +89 -9
  40. package/scripts/check-k8s.mjs +128 -569
  41. package/scripts/lint-conftest.mjs +98 -3
  42. package/scripts/lint-ga.mjs +18 -132
  43. package/scripts/lint-rego.mjs +19 -4
  44. package/scripts/utils/run-conftest-batch.mjs +117 -0
@@ -53,8 +53,8 @@ import { parseAllDocuments } from 'yaml'
53
53
 
54
54
  import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
55
55
  import { createCheckReporter } from './utils/check-reporter.mjs'
56
- import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
57
56
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
57
+ import { runConftestBatch } from './utils/run-conftest-batch.mjs'
58
58
  import { walkDir } from './utils/walkDir.mjs'
59
59
 
60
60
  const CONFIG_FILE = '.n-cursor.json'
@@ -100,7 +100,6 @@ const PATCH_PARENT_REF_NS_UA_RE =
100
100
  const PATCH_PARENT_REF_NS_RU_RE =
101
101
  /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
102
102
  const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
103
- const LEADING_EMPTY_LINE_RE = /^\s*\n/u
104
103
  const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
105
104
  const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}?op:\s*remove\b/mu
106
105
  const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
@@ -123,9 +122,6 @@ const HTTPROUTE_BACKENDREF_PORT_8081_RE =
123
122
  const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
124
123
  /value:\s*8081\b[\s\S]{0,200}?path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b/mu
125
124
 
126
- /** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
127
- export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
128
-
129
125
  /**
130
126
  * Регекс basename env-файлу abie: `dev.env` / `ua.env` / `ru.env`, опційно з провідною крапкою (`.dev.env` тощо).
131
127
  * Файл рівно `.env` (без імені) — виключення з правила: локальний файл розробника, `check-abie` його не сканує
@@ -275,115 +271,11 @@ export function isAbieK8sBaseYamlPath(rel) {
275
271
  return BASE_SEGMENT_RE.test(norm)
276
272
  }
277
273
 
278
- /**
279
- * Чи **hostname** дозволений для **HTTPRoute** у **base** (dev): **aiml.live**, **\*.aiml.live** або **\*.…\.aiml.live** (без урахування регістру).
280
- * @param {string} hostname значення з **spec.hostnames**
281
- * @returns {boolean} **true**, якщо hostname відповідає abie.mdc
282
- */
283
- export function isAllowedAbieBaseDevHostname(hostname) {
284
- if (typeof hostname !== 'string') {
285
- return false
286
- }
287
- const h = hostname.trim().toLowerCase()
288
- if (h === '') {
289
- return false
290
- }
291
- const root = ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT
292
- if (h === root) {
293
- return true
294
- }
295
- if (h === `*.${root}`) {
296
- return true
297
- }
298
- if (h.endsWith(`.${root}`)) {
299
- return true
300
- }
301
- return false
302
- }
303
-
304
- /**
305
- * @param {unknown} hostnames значення поля spec.hostnames
306
- * @returns {string[]} непорожні рядки-хости
307
- */
308
- function collectAbieHostnames(hostnames) {
309
- if (Array.isArray(hostnames)) {
310
- return hostnames.filter(h => typeof h === 'string' && h.trim() !== '')
311
- }
312
- if (typeof hostnames === 'string' && hostnames.trim() !== '') {
313
- return [hostnames]
314
- }
315
- return []
316
- }
317
-
318
- /**
319
- * Повідомлення про недопустимі **spec.hostnames** у **HTTPRoute** у шляху **…/base/…** (abie.mdc).
320
- * @param {unknown} obj корінь YAML-документа
321
- * @param {string} rel відносний шлях від кореня репозиторію
322
- * @returns {string[]} порожньо, якщо перевірка не застосовується або hostnames коректні
323
- */
324
- export function abieBaseHttpRouteHostnamesErrors(obj, rel) {
325
- if (!isAbieK8sBaseYamlPath(rel)) return []
326
- if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
327
- const rec = /** @type {Record<string, unknown>} */ (obj)
328
- if (rec.kind !== 'HTTPRoute') return []
329
- const spec = rec.spec
330
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return []
331
- const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
332
- if (hostnames === undefined) return []
333
- const hosts = collectAbieHostnames(hostnames)
334
- if (hosts.length === 0) return []
335
- const root = ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT
336
- return hosts
337
- .filter(h => !isAllowedAbieBaseDevHostname(h))
338
- .map(
339
- h =>
340
- `${rel}: HTTPRoute у base (dev): hostname "${h}" недопустимий — дозволені лише ${root} та піддомени, зокрема *.${root} (abie.mdc)`
341
- )
342
- }
343
-
344
- /**
345
- * Чи значення **`preem`** у base **Deployment** вважається «істинним» за abie.mdc (**true** або рядок **`true`** без урахування регістру).
346
- * @param {unknown} v значення з YAML
347
- * @returns {boolean} **true**, якщо значення вважається істинним за abie.mdc
348
- */
349
- function isAbiePreemTruthy(v) {
350
- if (v === true) {
351
- return true
352
- }
353
- if (typeof v === 'string' && v.trim().toLowerCase() === 'true') {
354
- return true
355
- }
356
- return false
357
- }
358
-
359
- /**
360
- * Чи документ **Deployment** у **`…/base/…`** містить **`spec.template.spec.nodeSelector.preem`** зі значенням **true** (abie.mdc).
361
- * @param {unknown} obj корінь YAML-документа (**Deployment**)
362
- * @returns {boolean} true, якщо критерії виконано
363
- */
364
- export function deploymentDocumentHasAbieBasePreemNodeSelector(obj) {
365
- if (!isDeploymentDoc(obj)) {
366
- return false
367
- }
368
- const rec = /** @type {Record<string, unknown>} */ (obj)
369
- const spec = rec.spec
370
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
371
- return false
372
- }
373
- const template = /** @type {Record<string, unknown>} */ (spec).template
374
- if (template === null || typeof template !== 'object' || Array.isArray(template)) {
375
- return false
376
- }
377
- const podSpec = /** @type {Record<string, unknown>} */ (template).spec
378
- if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) {
379
- return false
380
- }
381
- const nodeSelector = /** @type {Record<string, unknown>} */ (podSpec).nodeSelector
382
- if (nodeSelector === null || typeof nodeSelector !== 'object' || Array.isArray(nodeSelector)) {
383
- return false
384
- }
385
- return isAbiePreemTruthy(nodeSelector.preem)
386
- }
274
+ // Per-document валідація hostnames у `…/k8s/.../base/.../*.yaml` HTTPRoute
275
+ // (Plan B: Rego-authoritative) повністю в `npm/policy/abie/http_route_base/`.
276
+ // Per-document валідація `nodeSelector.preem` для Deployment у base — у
277
+ // `npm/policy/abie/base_deployment_preem/`. JS у `check-abie.mjs` робить лише
278
+ // path-фільтрацію + батч-виклик conftest через `runConftestBatch`.
387
279
 
388
280
  /**
389
281
  * Чи увімкнено правило **abie** у конфігу репозиторію.
@@ -414,46 +306,10 @@ export async function isAbieRuleEnabled(root) {
414
306
  return rules.some(r => String(r).trim().toLowerCase() === 'abie')
415
307
  }
416
308
 
417
- /**
418
- * Розбирає **`ignore_branches`** з workflow **clean-merged-branch** (крок delete-abandoned-branches).
419
- * @param {string} content вміст **.yml**
420
- * @returns {string | null} рядок **ignore_branches** або **null**
421
- */
422
- export function parseCleanMergedIgnoreBranches(content) {
423
- const root = parseWorkflowYaml(content)
424
- if (!root) {
425
- return null
426
- }
427
- for (const { step } of flattenWorkflowSteps(root)) {
428
- const uses = getStepUses(step)
429
- if (uses.includes('phpdocker-io/github-actions-delete-abandoned-branches')) {
430
- const w = step.with
431
- if (w && typeof w === 'object' && !Array.isArray(w)) {
432
- const ib = /** @type {Record<string, unknown>} */ (w).ignore_branches
433
- if (typeof ib === 'string') {
434
- return ib
435
- }
436
- }
437
- }
438
- }
439
- return null
440
- }
441
-
442
- /**
443
- * Чи рядок **ignore_branches** містить усі гілки з **required** (для abie — dev, ua, ru).
444
- * @param {string} ignoreBranches значення **ignore_branches**
445
- * @param {string[]} required імена гілок (нижній регістр для порівняння)
446
- * @returns {boolean} true, якщо всі **required** присутні як окремі токени
447
- */
448
- export function ignoreBranchesIncludesRequired(ignoreBranches, required) {
449
- const parts = new Set(
450
- ignoreBranches
451
- .split(',')
452
- .map(s => s.trim().toLowerCase())
453
- .filter(Boolean)
454
- )
455
- return required.every(r => parts.has(r.toLowerCase()))
456
- }
309
+ // Per-document валідація `clean-merged-branch.yml` (with.ignore_branches з
310
+ // dev/ua/ru) делегована rego-пакету `abie.clean_merged_ignore_branches`
311
+ // (`npm/policy/abie/clean_merged_ignore_branches/`). JS викликає
312
+ // `runConftestBatch` у `checkCleanMergedBranch`.
457
313
 
458
314
  /**
459
315
  * Збирає абсолютні шляхи до **.yaml** / **.yml** під деревом, де є сегмент **k8s**.
@@ -895,33 +751,11 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
895
751
  return dirs
896
752
  }
897
753
 
898
- /**
899
- * Перевіряє документи з одного файлу на наявність Deployment з preem nodeSelector.
900
- * @param {import('yaml').Document[]} docs документи з файлу
901
- * @param {string} rel відносний шлях файлу
902
- * @param {(msg: string) => void} fail callback
903
- * @returns {'violation' | 'found' | 'none'} результат перевірки
904
- */
905
- function checkBaseDeploymentDocsForPreem(docs, rel, fail) {
906
- for (const doc of docs) {
907
- if (doc.errors.length === 0) {
908
- const obj = doc.toJSON()
909
- if (isDeploymentDoc(obj)) {
910
- if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
911
- fail(
912
- `${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
913
- )
914
- return 'violation'
915
- }
916
- return 'found'
917
- }
918
- }
919
- }
920
- return 'none'
921
- }
922
-
923
754
  /**
924
755
  * Для кожного **Deployment** у YAML під **`k8s`** з шляхом **`…/base/…`** вимагає **`spec.template.spec.nodeSelector.preem: true`** (abie.mdc).
756
+ *
757
+ * Per-document валідація делегована у rego-пакет **`abie.base_deployment_preem`**
758
+ * (`npm/policy/abie/base_deployment_preem/`) — JS лише фільтрує файли за path-патерном `base/` і батчем спавнить conftest.
925
759
  * @param {string} root корінь репозиторію
926
760
  * @param {string[]} yamlFilesAbs yaml під k8s
927
761
  * @param {(msg: string) => void} fail callback
@@ -930,19 +764,21 @@ function checkBaseDeploymentDocsForPreem(docs, rel, fail) {
930
764
  */
931
765
  async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
932
766
  const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
933
- let anyBaseDeployment = false
934
- for (const abs of baseFiles) {
935
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
936
- const docs = await readAndParseYamlDocs(abs, rel, fail)
937
- if (!docs) return
938
- const r = checkBaseDeploymentDocsForPreem(docs, rel, fail)
939
- if (r === 'violation') return
940
- if (r === 'found') anyBaseDeployment = true
767
+ if (baseFiles.length === 0) {
768
+ passFn('Немає файлів у шляхах …/base/… — перевірку preem у base пропущено')
769
+ return
941
770
  }
942
- if (anyBaseDeployment) {
943
- passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
944
- } else {
945
- passFn('Немає Deployment у шляхах …/base/… — перевірку preem у base пропущено')
771
+ const violations = runConftestBatch({
772
+ policyDirRel: 'abie/base_deployment_preem',
773
+ namespace: 'abie.base_deployment_preem',
774
+ files: baseFiles
775
+ })
776
+ for (const v of violations) {
777
+ const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
778
+ fail(`${rel}: ${v.message}`)
779
+ }
780
+ if (violations.length === 0) {
781
+ passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc (rego)')
946
782
  }
947
783
  }
948
784
 
@@ -1363,89 +1199,22 @@ export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
1363
1199
  return validateAbieNginxRunHttpRoutePatches(combined, mode, raw) === null
1364
1200
  }
1365
1201
 
1366
- /**
1367
- * Перевіряє об'єкт HealthCheckPolicy на відповідність abie.mdc.
1368
- * @param {Record<string, unknown>} policy розібраний HealthCheckPolicy
1369
- * @param {string} relPath відносний шлях (для повідомлень)
1370
- * @returns {string | null} текст помилки або null якщо OK
1371
- */
1372
- function validateAbieHcPolicy(policy, relPath) {
1373
- if (policy.apiVersion !== 'networking.gke.io/v1') {
1374
- return `${relPath}: apiVersion має бути networking.gke.io/v1 (abie.mdc)`
1375
- }
1376
- const meta = policy.metadata
1377
- if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
1378
- return `${relPath}: відсутній metadata (abie.mdc)`
1379
- }
1380
- const name = /** @type {Record<string, unknown>} */ (meta).name
1381
- if (typeof name !== 'string' || name.trim() === '') {
1382
- return `${relPath}: metadata.name має бути непорожнім рядком (abie.mdc)`
1383
- }
1384
- const spec = policy.spec
1385
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
1386
- return `${relPath}: відсутній spec (abie.mdc)`
1387
- }
1388
- const specRec = /** @type {Record<string, unknown>} */ (spec)
1389
- const def = specRec.default
1390
- if (def === null || typeof def !== 'object' || Array.isArray(def)) {
1391
- return `${relPath}: відсутній spec.default (abie.mdc)`
1392
- }
1393
- const config = /** @type {Record<string, unknown>} */ (def).config
1394
- if (config === null || typeof config !== 'object' || Array.isArray(config)) {
1395
- return `${relPath}: відсутній spec.default.config (abie.mdc)`
1396
- }
1397
- if (config.type !== 'HTTP') return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
1398
- const httpHc = /** @type {Record<string, unknown>} */ (config).httpHealthCheck
1399
- if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
1400
- return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
1401
- }
1402
- const requestPath = typeof httpHc.requestPath === 'string' ? httpHc.requestPath.trim() : ''
1403
- if (requestPath === '' || !requestPath.startsWith('/')) {
1404
- return `${relPath}: httpHealthCheck.requestPath має бути непорожнім шляхом від кореня (рядок, що починається з /) (abie.mdc)`
1405
- }
1406
- if (httpHc.port !== 8080) return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
1407
- const targetRef = specRec.targetRef
1408
- if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
1409
- return `${relPath}: відсутній targetRef (abie.mdc)`
1410
- }
1411
- const tr = /** @type {Record<string, unknown>} */ (targetRef)
1412
- if (tr.kind !== 'Service') return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
1413
- const expectedHl = name.endsWith('-hl') ? name : `${name}-hl`
1414
- if (typeof tr.name !== 'string' || tr.name !== expectedHl) {
1415
- return `${relPath}: targetRef.name має посилатися на headless Service (очікується ${expectedHl}, суфікс -hl) (abie.mdc)`
1416
- }
1417
- return null
1418
- }
1202
+ // Per-document валідація HealthCheckPolicy (apiVersion / spec.default.config /
1203
+ // httpHealthCheck / targetRef з суфіксом `-hl` exact match) делегована
1204
+ // rego-пакету `abie.health_check_policy` (`npm/policy/abie/health_check_policy/`).
1205
+ // JS у `checkHcYamlFiles` робить лише modeline-перевірку (`validateAbieHcModeline`)
1206
+ // і батч-виклик conftest.
1419
1207
 
1420
1208
  /**
1421
- * Шукає HealthCheckPolicy серед YAML-документів.
1422
- * @param {import('yaml').Document[]} docs документи
1423
- * @param {string} relPath відносний шлях для повідомлень
1424
- * @returns {{ policy: Record<string, unknown> } | { error: string }} знайдений документ або помилка
1425
- */
1426
- function findHealthCheckPolicyInDocs(docs, relPath) {
1427
- for (const doc of docs) {
1428
- if (doc.errors.length > 0) {
1429
- return { error: `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}` }
1430
- }
1431
- const obj = doc.toJSON()
1432
- if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
1433
- const rec = /** @type {Record<string, unknown>} */ (obj)
1434
- if (rec.kind === 'HealthCheckPolicy') {
1435
- return { policy: rec }
1436
- }
1437
- }
1438
- }
1439
- return { policy: /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (null)) }
1440
- }
1441
-
1442
- /**
1443
- * Перевіряє hc.yaml на відповідність схемі та структурі HealthCheckPolicy (abie.mdc).
1209
+ * JS-частина перевірки hc.yaml — лише modeline (`# yaml-language-server: $schema=…`).
1210
+ * Парсинг YAML і структурна валідація HealthCheckPolicy делеговано в rego-пакет
1211
+ * **`abie.health_check_policy`** (`npm/policy/abie/health_check_policy/`),
1212
+ * викликається з `checkHcYamlFile` через `runConftestBatch`.
1444
1213
  * @param {string} raw вміст файлу
1445
1214
  * @param {string} relPath відносний шлях (для повідомлень)
1446
1215
  * @returns {string | null} null якщо OK, рядок з помилкою
1447
1216
  */
1448
- export function validateAbieHcYaml(raw, relPath) {
1217
+ export function validateAbieHcModeline(raw, relPath) {
1449
1218
  const body = stripBom(raw)
1450
1219
  const lines = body.split(LINE_SPLIT_RE)
1451
1220
  if (lines.length === 0 || lines[0].trim() === '') {
@@ -1454,20 +1223,7 @@ export function validateAbieHcYaml(raw, relPath) {
1454
1223
  const m = lines[0].match(MODELINE_RE)
1455
1224
  if (!m) return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
1456
1225
  if (m[1] !== ABIE_HC_SCHEMA_URL) return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
1457
-
1458
- const yamlBody = lines.slice(1).join('\n').replace(LEADING_EMPTY_LINE_RE, '')
1459
- /** @type {import('yaml').Document[]} */
1460
- let docs
1461
- try {
1462
- docs = parseAllDocuments(yamlBody)
1463
- } catch (error) {
1464
- const msg = error instanceof Error ? error.message : String(error)
1465
- return `${relPath}: не вдалося розібрати YAML (${msg})`
1466
- }
1467
- const result = findHealthCheckPolicyInDocs(docs, relPath)
1468
- if ('error' in result) return result.error
1469
- if (!result.policy) return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
1470
- return validateAbieHcPolicy(result.policy, relPath)
1226
+ return null
1471
1227
  }
1472
1228
 
1473
1229
  /**
@@ -1663,37 +1419,6 @@ async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, c
1663
1419
  return true
1664
1420
  }
1665
1421
 
1666
- /**
1667
- * @param {unknown} json YAML-документ
1668
- * @returns {boolean} true, якщо HTTPRoute має непорожні spec.hostnames
1669
- */
1670
- function httpRouteHasNonEmptyHostnames(json) {
1671
- if (json === null || typeof json !== 'object' || Array.isArray(json)) return false
1672
- const rec = /** @type {Record<string, unknown>} */ (json)
1673
- if (rec.kind !== 'HTTPRoute') return false
1674
- const spec = rec.spec
1675
- if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
1676
- const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
1677
- return collectAbieHostnames(hostnames).length > 0
1678
- }
1679
-
1680
- /**
1681
- * @param {import('yaml').Document} doc YAML-документ з файлу
1682
- * @param {string} rel відносний шлях для повідомлень
1683
- * @param {(msg: string) => void} fail callback при помилці
1684
- * @returns {{ hasErrors: boolean, hasHostnames: boolean }} результат обробки документа
1685
- */
1686
- function processBaseHttpRouteDoc(doc, rel, fail) {
1687
- if (doc.errors.length !== 0) return { hasErrors: false, hasHostnames: false }
1688
- const json = doc.toJSON()
1689
- const errs = abieBaseHttpRouteHostnamesErrors(json, rel)
1690
- if (errs.length > 0) {
1691
- for (const e of errs) fail(e)
1692
- return { hasErrors: true, hasHostnames: false }
1693
- }
1694
- return { hasErrors: false, hasHostnames: httpRouteHasNonEmptyHostnames(json) }
1695
- }
1696
-
1697
1422
  /**
1698
1423
  * Для кожного **HTTPRoute** у **`…/k8s/base/…`** з непорожніми **`spec.hostnames`** — лише **aiml.live** та піддомени (abie.mdc).
1699
1424
  * @param {string} root корінь репозиторію
@@ -1703,24 +1428,26 @@ function processBaseHttpRouteDoc(doc, rel, fail) {
1703
1428
  * @returns {Promise<void>}
1704
1429
  */
1705
1430
  async function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
1706
- let baseHttpRoutesWithHostnames = 0
1707
1431
  const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
1708
- for (const abs of baseFiles) {
1709
- const rel = relative(root, abs).replaceAll('\\', '/') || abs
1710
- const docs = await readAndParseYamlDocs(abs, rel, fail)
1711
- if (!docs) return
1712
- for (const doc of docs) {
1713
- const { hasErrors, hasHostnames } = processBaseHttpRouteDoc(doc, rel, fail)
1714
- if (hasErrors) return
1715
- if (hasHostnames) baseHttpRoutesWithHostnames++
1716
- }
1432
+ if (baseFiles.length === 0) {
1433
+ passFn('Немає файлів у шляхах …/k8s/base/… — перевірку HTTPRoute hostnames пропущено')
1434
+ return
1435
+ }
1436
+ // Per-document валідація делегована rego-пакету `abie.http_route_base`
1437
+ // (`npm/policy/abie/http_route_base/`) rego гейтує по `kind == "HTTPRoute"`.
1438
+ const violations = runConftestBatch({
1439
+ policyDirRel: 'abie/http_route_base',
1440
+ namespace: 'abie.http_route_base',
1441
+ files: baseFiles
1442
+ })
1443
+ for (const v of violations) {
1444
+ const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
1445
+ fail(`${rel}: ${v.message}`)
1717
1446
  }
1718
- if (baseHttpRoutesWithHostnames > 0) {
1447
+ if (violations.length === 0) {
1719
1448
  passFn(
1720
- `HTTPRoute у …/k8s/base/…: spec.hostnames відповідають ${ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT} та піддоменам (abie.mdc)`
1449
+ `HTTPRoute у …/k8s/base/…: spec.hostnames відповідають ${ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT} та піддоменам (rego)`
1721
1450
  )
1722
- } else {
1723
- passFn('Немає HTTPRoute у …/k8s/base/… з непорожніми spec.hostnames — перевірку aiml.live пропущено')
1724
1451
  }
1725
1452
  }
1726
1453
 
@@ -1811,23 +1538,16 @@ async function checkCleanMergedBranch(root, pass, fail) {
1811
1538
  fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
1812
1539
  return
1813
1540
  }
1814
- let wfRaw
1815
- try {
1816
- wfRaw = await readFile(cleanMergedPath, 'utf8')
1817
- } catch (error) {
1818
- const msg = error instanceof Error ? error.message : String(error)
1819
- fail(`Не вдалося прочитати clean-merged-branch.yml (${msg})`)
1820
- return
1821
- }
1822
- const ib = parseCleanMergedIgnoreBranches(wfRaw)
1823
- if (ib === null || ib.trim() === '') {
1824
- fail(
1825
- 'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
1826
- )
1827
- } else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
1828
- pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
1829
- } else {
1830
- fail(`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`)
1541
+ // Per-document валідація делегована у rego-пакет `abie.clean_merged_ignore_branches`
1542
+ // (`npm/policy/abie/clean_merged_ignore_branches/`). conftest сам читає та парсить YAML.
1543
+ const violations = runConftestBatch({
1544
+ policyDirRel: 'abie/clean_merged_ignore_branches',
1545
+ namespace: 'abie.clean_merged_ignore_branches',
1546
+ files: [cleanMergedPath]
1547
+ })
1548
+ for (const v of violations) fail(v.message)
1549
+ if (violations.length === 0) {
1550
+ pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru (rego)')
1831
1551
  }
1832
1552
  }
1833
1553
 
@@ -1838,39 +1558,52 @@ async function checkCleanMergedBranch(root, pass, fail) {
1838
1558
  * @param {(msg: string) => void} pass callback при успішній перевірці
1839
1559
  * @param {(msg: string) => void} fail callback при помилці
1840
1560
  */
1841
- async function checkHcYamlFile(root, dir, pass, fail) {
1842
- const hcAbs = join(dir, 'hc.yaml')
1843
- const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
1844
- if (!existsSync(hcAbs)) {
1845
- fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
1846
- return
1847
- }
1848
- let hcRaw
1849
- try {
1850
- hcRaw = await readFile(hcAbs, 'utf8')
1851
- } catch (error) {
1852
- const msg = error instanceof Error ? error.message : String(error)
1853
- fail(`${relHc}: не вдалося прочитати (${msg})`)
1854
- return
1855
- }
1856
- const v = validateAbieHcYaml(hcRaw, relHc)
1857
- if (v === null) {
1858
- pass(`${relHc}: відповідає abie.mdc`)
1859
- } else {
1860
- fail(v)
1861
- }
1862
- }
1863
-
1864
1561
  /**
1865
- * Перевіряє hc.yaml у директоріях з Deployment.
1562
+ * Перевіряє hc.yaml у директоріях з Deployment. JS перевіряє modeline, далі
1563
+ * один батч conftest для усіх знайдених hc.yaml — структурна валідація HCP
1564
+ * делегується rego (`abie.health_check_policy`).
1866
1565
  * @param {string} root корінь репозиторію
1867
1566
  * @param {Set<string>} deploymentDirs директорії з Deployment (Set)
1868
1567
  * @param {(msg: string) => void} pass callback при успішній перевірці
1869
1568
  * @param {(msg: string) => void} fail callback при помилці
1870
1569
  */
1871
1570
  async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
1571
+ /** @type {string[]} файли, які пройшли modeline-check і йдуть у conftest */
1572
+ const hcFilesForRego = []
1872
1573
  for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
1873
- await checkHcYamlFile(root, dir, pass, fail)
1574
+ const hcAbs = join(dir, 'hc.yaml')
1575
+ const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
1576
+ if (!existsSync(hcAbs)) {
1577
+ fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
1578
+ continue
1579
+ }
1580
+ let hcRaw
1581
+ try {
1582
+ hcRaw = await readFile(hcAbs, 'utf8')
1583
+ } catch (error) {
1584
+ const msg = error instanceof Error ? error.message : String(error)
1585
+ fail(`${relHc}: не вдалося прочитати (${msg})`)
1586
+ continue
1587
+ }
1588
+ const modelineErr = validateAbieHcModeline(hcRaw, relHc)
1589
+ if (modelineErr !== null) {
1590
+ fail(modelineErr)
1591
+ continue
1592
+ }
1593
+ hcFilesForRego.push(hcAbs)
1594
+ }
1595
+ if (hcFilesForRego.length === 0) return
1596
+ const violations = runConftestBatch({
1597
+ policyDirRel: 'abie/health_check_policy',
1598
+ namespace: 'abie.health_check_policy',
1599
+ files: hcFilesForRego
1600
+ })
1601
+ for (const v of violations) {
1602
+ const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
1603
+ fail(`${rel}: ${v.message}`)
1604
+ }
1605
+ if (violations.length === 0 && hcFilesForRego.length > 0) {
1606
+ pass(`HealthCheckPolicy: ${hcFilesForRego.length} файл(ів) hc.yaml відповідають abie.mdc (rego)`)
1874
1607
  }
1875
1608
  }
1876
1609