@7n/rules 1.8.0 → 1.8.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.2] - 2026-07-17
4
+
5
+ ### Fixed
6
+
7
+ - bun/licensee: розрізняти crash інструмента (stderr) від реального ліцензійного порушення (stdout, --errors-only)
8
+ - k8s/manifests: знизити когнітивну складність ensureHasuraConfigMapRequiredEnv нижче порогу sonarjs
9
+
10
+ ## [1.8.1] - 2026-07-17
11
+
12
+ ### Changed
13
+
14
+ - docs(adr): нормалізувати чернетки ADR у канонічний MADR-формат
15
+
3
16
  ## [1.8.0] - 2026-07-16
4
17
 
5
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,35 +3,28 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/bun/licensee/main.mjs
5
5
  docgen:
6
- crc: bbaca446
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- score: 90
9
- issues: surzhik,judge:inaccurate:0.96
10
- judgeModel: openai-codex/gpt-5.4-mini
6
+ crc: 48c72e2b
7
+ model: manual
11
8
  ---
12
9
 
13
10
  ## Огляд
14
11
 
15
- Встановлює конфігурацію ліцензій, спираючись на файл .licensee.json, якщо він відсутній. Виконує перевірку відповідності ліцензій усіх npm-залежностей проєкту встановленим вимогам, що визначаються в конфігурації. У випадку виявлення порушень, система повідомляє про них, підтверджуючи чисту відповідність або фіксуючи розбіжності. Маркери повідомлень згадуються у Поведінці: (bun.mdc).
12
+ Read-only detector `bun/licensee`: перевіряє ліцензії npm-залежностей проєкту через зовнішній інструмент `licensee` (`bun x licensee`), звіряючи їх з allowlist у `.licensee.json`. Розрізняє два принципово різні провали: `licensee` знайшов реально заборонену ліцензію (`license-violation`) і `licensee` сам впав з помилкою (`licensee-crashed`), наприклад через несумісність `@npmcli/arborist` (яким він читає `node_modules`) з деревом, зібраним `bun install`.
16
13
 
17
14
  ## Поведінка
18
15
 
19
- Поведінка:
20
-
21
- 1. Викликається `lint` для запуску перевірки ліцензій npm-залежностей.
22
- 2. Якщо файл конфігурації `.licensee.json` відсутній:
23
- a. Якщо режим читання лише (`readOnly`) увімкнено, повідомляється, що необхідно запустити `npx @7n/rules fix bun` локально для генерації (bun.mdc).
24
- b. Якщо режим читання лише вимкнено, створюється `.licensee.json` з дефолтним allowlist (`blueOak: bronze`) та повідомляється про створення конфігурації.
25
- 3. Визначається команда `bun`. Якщо `bun` не знайдена у системному шляху, повідомляється про це (bun.mdc).
26
- 4. Виконується команда `bun x licensee --production --quiet` у корені проєкту.
27
- 5. Якщо команда `licensee` завершується успішно (статус 0), повідомляється, що ліцензії відповідають вимогам.
28
- 6. Якщо команда `licensee` завершується з помилкою (не 0), повідомляється про порушення ліцензій (код) (bun.mdc).
16
+ 1. Якщо `.licensee.json` відсутній — `licensee-config-missing`; генерація конфігу — окремий T0-фікс (`fix-licensee.mjs`), не тут.
17
+ 2. Якщо `bun` не знайдено в PATH — `bun-missing`.
18
+ 3. Запускає `bun x licensee --production --errors-only` (без `--quiet` реальне порушення потребує деталі: ім'я/версія/ліцензія пакета, які `licensee` пише в stdout через `print()`).
19
+ 4. Статус 0 без порушень.
20
+ 5. Статус 0 і є вивід у **stderr** — трактується як crash/`die()` самого `licensee` (invalid config або необроблений виняток усередині тула), НЕ як ліцензійне порушення: `licensee-crashed`, повідомлення включає stderr і підказку перевірити вручну `bunx licensee --production`.
21
+ 6. Статус 0 і stderr порожній, є вивід у **stdout** — це справжній `NOT APPROVED`-звіт від `licensee` (`--errors-only` друкує лише невідповідні пакети): `license-violation` з деталлю пакета/ліцензії у повідомленні.
29
22
 
30
23
  ## Публічний API
31
24
 
32
- lint — аналізує код на відповідність заданим стилістичним нормам у контексті `bun`.
33
- licensee — перевіряє відповідність ліцензій усіх використаних npm-залежностей файлу `.licensee.json`.
25
+ - `lint(ctx)`detector-контракт unified lint surface; повертає `{ violations }`.
34
26
 
35
27
  ## Гарантії поведінки
36
28
 
37
- - (специфічних машинно-виведених гарантій немає)
29
+ - Read-only: не пише `.licensee.json` і не змінює `node_modules`/lockfile сам (побічний ефект `bun x` — авто-встановлення пакета `licensee`, якщо відсутній у bunx-кеші, — керується bun, не цим модулем).
30
+ - Канал stdout/stderr — єдиний сигнал розрізнення crash vs реальне порушення; текстові евристики за вмістом повідомлення не використовуються.
@@ -34,11 +34,27 @@ export function lint(ctx) {
34
34
  return reporter.result()
35
35
  }
36
36
 
37
- const r = spawnSync(bun, ['x', 'licensee', '--production', '--quiet'], { cwd, encoding: 'utf8', shell: false })
37
+ // Без --quiet: `licensee` пише реальні NOT APPROVED записи (name/version/license) у
38
+ // stdout через print() — деталь замість голого "код 1". Crash/die() усередині
39
+ // самого тула (invalid config, внутрішній виняток на кшталт "Cannot read properties
40
+ // of undefined (reading 'localeCompare')" — спостережено з @npmcli/arborist на
41
+ // bun-based node_modules) завжди йде у stderr через die(); легітимний звіт про
42
+ // порушення — лише у stdout. Канал розрізняє crash від реального порушення.
43
+ const r = spawnSync(bun, ['x', 'licensee', '--production', '--errors-only'], { cwd, encoding: 'utf8', shell: false })
38
44
  if (r.status !== 0) {
39
- const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
40
- const detail = out ? `\n${out}` : ''
41
- fail(`lint-bun: licensee — порушення ліцензій (код ${r.status}, bun.mdc)${detail}`, 'license-violation')
45
+ const stderr = (r.stderr ?? '').trim().slice(0, 2000)
46
+ if (stderr) {
47
+ fail(
48
+ `lint-bun: licensee — інструмент завершився з помилкою, це НЕ підтверджене ліцензійне порушення ` +
49
+ `(код ${r.status}, bun.mdc). Ймовірна причина — несумісність @npmcli/arborist (яким licensee читає ` +
50
+ `node_modules) з деревом, зібраним bun install. Перевір вручну: \`bunx licensee --production\`.\n${stderr}`,
51
+ 'licensee-crashed'
52
+ )
53
+ } else {
54
+ const stdout = (r.stdout ?? '').trim().slice(0, 2000)
55
+ const detail = stdout ? `\n${stdout}` : ''
56
+ fail(`lint-bun: licensee — порушення ліцензій (код ${r.status}, bun.mdc)${detail}`, 'license-violation')
57
+ }
42
58
  }
43
59
  return reporter.result()
44
60
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-manifests.mjs
4
4
  resource: npm/rules/k8s/manifests/fix-manifests.mjs
5
5
  docgen:
6
- crc: 7245cdbe
6
+ crc: e0c44174
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -7,7 +7,13 @@
7
7
  * - `batch-v1beta1-apiversion` — apiVersion batch/v1beta1 → batch/v1 (CronJob/Job);
8
8
  * - `schema-modeline-first` — перемістити `# yaml-language-server: $schema=…` у перший рядок;
9
9
  * - `kustomization-patches-sort` — впорядкувати `patches[]` (реюз детекторних sort-ключів);
10
- * - `deployment-strategy` — проставити канонічний `spec.strategy` RollingUpdate.
10
+ * - `deployment-strategy` — проставити канонічний `spec.strategy` RollingUpdate;
11
+ * - `hasura-configmap-env` — проставити обов'язкові `HASURA_GRAPHQL_*` env у Hasura ConfigMap;
12
+ * - `hasura-httproute-rule1-filters` — проставити канонічний RequestRedirect у правило 1
13
+ * Hasura-канона HTTPRoute (лише overwrite існуючого правила, без синтезу нових);
14
+ * - `svc-clusterip-type` — проставити `spec.type: ClusterIP` у `svc.yaml`;
15
+ * - `svc-hl-cluster-ip` — проставити `spec.clusterIP: None` у `svc-hl.yaml`
16
+ * (без перейменування `metadata.name`, суфікс `-hl` — не T0).
11
17
  *
12
18
  * Правки роблять через `yaml` Document/Documents (зберігають коментарі), окрім modeline
13
19
  * (чистий текст). Семантичну коректність гарантує canonical re-detect (rego) — T0
@@ -20,6 +26,9 @@ import { parseAllDocuments, parseDocument } from 'yaml'
20
26
 
21
27
  import {
22
28
  compareStringTuplesEn,
29
+ findHasuraCanonStart,
30
+ HASURA_REQUIRED_ENV_VALUES,
31
+ hasuraRuleHasExactRedirect,
23
32
  kustomizationPatchSortKey,
24
33
  loadSnippetSpec,
25
34
  replaceBatchV1beta1ApiVersionInYamlText,
@@ -127,6 +136,161 @@ export function ensureNetworkPolicyEgress(content) {
127
136
  return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
128
137
  }
129
138
 
139
+ /**
140
+ * Чи значення читається як логічне `true` (boolean або рядок, case-insensitive) —
141
+ * дзеркалить `is_value_true` з `k8s.hasura_configmap.rego`.
142
+ * @param {unknown} v значення з `data`
143
+ * @returns {boolean} true, якщо значення означає true
144
+ */
145
+ function isTruthyBool(v) {
146
+ if (v === true) return true
147
+ return typeof v === 'string' && v.trim().toLowerCase() === 'true'
148
+ }
149
+
150
+ /**
151
+ * Чи значення читається як логічне `false` — дзеркалить `is_value_false`.
152
+ * @param {unknown} v значення з `data`
153
+ * @returns {boolean} true, якщо значення означає false
154
+ */
155
+ function isFalsyBool(v) {
156
+ if (v === false) return true
157
+ return typeof v === 'string' && v.trim().toLowerCase() === 'false'
158
+ }
159
+
160
+ /**
161
+ * Визначає значення для одного `HASURA_GRAPHQL_*` ключа за очікуванням із
162
+ * `HASURA_REQUIRED_ENV_VALUES`: `null` — довільне значення, проставляється лише
163
+ * якщо ключ відсутній (дефолт `'true'`); `'true'`/`'false'` — булеве порівняння
164
+ * (case-insensitive, `isTruthyBool`/`isFalsyBool`); інакше — точний рядок.
165
+ * @param {string|null} expected очікуване значення з `HASURA_REQUIRED_ENV_VALUES`
166
+ * @param {unknown} current поточне значення ключа в `data` (може бути `undefined`)
167
+ * @returns {string|null} значення для `setIn`, або `null`, якщо змін не потрібно
168
+ */
169
+ function resolveHasuraEnvValue(expected, current) {
170
+ if (expected === null) return current === undefined ? 'true' : null
171
+ if (expected === 'true') return isTruthyBool(current) ? null : 'true'
172
+ if (expected === 'false') return isFalsyBool(current) ? null : 'false'
173
+ return current === expected ? null : expected
174
+ }
175
+
176
+ /**
177
+ * Проставляє обов'язкові `HASURA_GRAPHQL_*` env-ключі (`HASURA_REQUIRED_ENV_VALUES`,
178
+ * дзеркалить `k8s.hasura_configmap.rego`) у `data` документа `kind: ConfigMap`. Ключ з
179
+ * очікуванням `null` (наприклад `HASURA_GRAPHQL_DISABLE_EVENTING`) — довільне значення,
180
+ * T0 проставляє лише якщо ключ відсутній (дефолт `'true'`).
181
+ * @param {string} content вміст configmap.yaml
182
+ * @returns {string|null} новий вміст або null, якщо змін немає / парс невпевнений
183
+ */
184
+ export function ensureHasuraConfigMapRequiredEnv(content) {
185
+ let doc
186
+ try {
187
+ doc = parseDocument(content)
188
+ } catch {
189
+ return null
190
+ }
191
+ if (doc.errors?.length) return null
192
+ if (doc.get('kind') !== 'ConfigMap') return null
193
+
194
+ let changed = false
195
+ for (const [key, expected] of Object.entries(HASURA_REQUIRED_ENV_VALUES)) {
196
+ const value = resolveHasuraEnvValue(expected, doc.getIn(['data', key]))
197
+ if (value === null) continue
198
+ doc.setIn(['data', key], value)
199
+ changed = true
200
+ }
201
+ if (!changed) return null
202
+ return doc.toString()
203
+ }
204
+
205
+ /**
206
+ * Проставляє канонічний RequestRedirect (правило 1 Hasura-канона, `k8s.hasura_httproute.rego`)
207
+ * у вже знайдене правило `Exact "<prefix>/ql"`. Фіксує лише **існуюче** правило (overwrite
208
+ * `filters`) — правила 2-4 (rule2/3/4_missing) потребують синтезу нового правила з
209
+ * `backendRef`, якого нізвідки достовірно вивести, тож НЕ T0 (за тим самим принципом, що й
210
+ * `internal-url-invalid` у `hasura/internal_urls`: людське рішення про інфраструктуру).
211
+ * @param {string} content вміст hr.yaml (HTTPRoute, може бути multi-doc)
212
+ * @returns {string|null} новий вміст або null, якщо змін немає / не застосовно / парс невпевнений
213
+ */
214
+ export function ensureHasuraHttpRouteRule1Filters(content) {
215
+ let docs
216
+ try {
217
+ docs = parseAllDocuments(content)
218
+ } catch {
219
+ return null
220
+ }
221
+ if (docs.some(d => d.errors?.length)) return null
222
+ let changed = false
223
+ for (const doc of docs) {
224
+ if (doc.get('kind') !== 'HTTPRoute') continue
225
+ const plainRules = doc.toJSON()?.spec?.rules
226
+ if (!Array.isArray(plainRules) || plainRules.length === 0) continue
227
+ const start = findHasuraCanonStart(plainRules)
228
+ if (start === null) continue
229
+ const consolePath = `${start.prefix}/ql/console`
230
+ if (hasuraRuleHasExactRedirect(plainRules[start.startIndex], consolePath)) continue // вже канонічно
231
+ const canonicalFilters = [
232
+ {
233
+ type: 'RequestRedirect',
234
+ requestRedirect: { statusCode: 302, path: { type: 'ReplaceFullPath', replaceFullPath: consolePath } }
235
+ }
236
+ ]
237
+ doc.setIn(['spec', 'rules', start.startIndex, 'filters'], doc.createNode(canonicalFilters))
238
+ changed = true
239
+ }
240
+ if (!changed) return null
241
+ return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
242
+ }
243
+
244
+ /**
245
+ * Проставляє `spec.type: ClusterIP` у документ `kind: Service` (`svc.yaml`, `k8s.svc_yaml.rego`).
246
+ * @param {string} content вміст svc.yaml (може бути multi-doc)
247
+ * @returns {string|null} новий вміст або null, якщо змін немає / не застосовно / парс невпевнений
248
+ */
249
+ export function ensureSvcClusterIpType(content) {
250
+ let docs
251
+ try {
252
+ docs = parseAllDocuments(content)
253
+ } catch {
254
+ return null
255
+ }
256
+ if (docs.some(d => d.errors?.length)) return null
257
+ let changed = false
258
+ for (const doc of docs) {
259
+ if (doc.get('kind') !== 'Service') continue
260
+ if (doc.getIn(['spec', 'type']) === 'ClusterIP') continue
261
+ doc.setIn(['spec', 'type'], 'ClusterIP')
262
+ changed = true
263
+ }
264
+ if (!changed) return null
265
+ return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
266
+ }
267
+
268
+ /**
269
+ * Проставляє `spec.clusterIP: None` у документ `kind: Service` (`svc-hl.yaml`, `k8s.svc_hl_yaml.rego`).
270
+ * Не чіпає `metadata.name` (суфікс `-hl`) — перейменування ресурсу впливає на посилання з
271
+ * інших файлів (ConfigMap/Deployment/HTTPRoute), тож НЕ T0.
272
+ * @param {string} content вміст svc-hl.yaml (може бути multi-doc)
273
+ * @returns {string|null} новий вміст або null, якщо змін немає / не застосовно / парс невпевнений
274
+ */
275
+ export function ensureSvcHlClusterIp(content) {
276
+ let docs
277
+ try {
278
+ docs = parseAllDocuments(content)
279
+ } catch {
280
+ return null
281
+ }
282
+ if (docs.some(d => d.errors?.length)) return null
283
+ let changed = false
284
+ for (const doc of docs) {
285
+ if (doc.get('kind') !== 'Service') continue
286
+ if (doc.getIn(['spec', 'clusterIP']) === 'None') continue
287
+ doc.setIn(['spec', 'clusterIP'], 'None')
288
+ changed = true
289
+ }
290
+ if (!changed) return null
291
+ return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
292
+ }
293
+
130
294
  /**
131
295
  * Застосовує текстовий трансформер до унікальних файлів із targets і пише зміни.
132
296
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintViolation[]} targets violations із файлами для правки
@@ -224,5 +388,29 @@ export const patterns = [
224
388
  'networkpolicy-egress',
225
389
  ensureNetworkPolicyEgress,
226
390
  n => `канонічний spec.egress проставлено: ${n} файл(ів)`
391
+ ),
392
+ fileTransformPattern(
393
+ 'k8s-manifests-hasura-configmap-env',
394
+ 'hasura-configmap-env',
395
+ ensureHasuraConfigMapRequiredEnv,
396
+ n => `обов'язкові HASURA_GRAPHQL_* env проставлено: ${n} файл(ів)`
397
+ ),
398
+ fileTransformPattern(
399
+ 'k8s-manifests-hasura-httproute-rule1-filters',
400
+ 'hasura-httproute-rule1-filters',
401
+ ensureHasuraHttpRouteRule1Filters,
402
+ n => `правило 1 Hasura-канона (RequestRedirect) проставлено: ${n} файл(ів)`
403
+ ),
404
+ fileTransformPattern(
405
+ 'k8s-manifests-svc-clusterip-type',
406
+ 'svc-clusterip-type',
407
+ ensureSvcClusterIpType,
408
+ n => `svc.yaml spec.type: ClusterIP проставлено: ${n} файл(ів)`
409
+ ),
410
+ fileTransformPattern(
411
+ 'k8s-manifests-svc-hl-cluster-ip',
412
+ 'svc-hl-cluster-ip',
413
+ ensureSvcHlClusterIp,
414
+ n => `svc-hl.yaml spec.clusterIP: None проставлено: ${n} файл(ів)`
227
415
  )
228
416
  ]
@@ -2283,6 +2283,21 @@ export const HASURA_REQUIRED_ENV_KEYS = [
2283
2283
  'HASURA_GRAPHQL_DISABLE_EVENTING'
2284
2284
  ]
2285
2285
 
2286
+ /**
2287
+ * Очікувані значення `HASURA_REQUIRED_ENV_KEYS` (дзеркалить `k8s.hasura_configmap.rego`).
2288
+ * `'true'`/`'false'` — приймаються як boolean або рядок (case-insensitive); рядкове значення
2289
+ * (наприклад `ENABLED_LOG_TYPES`) — точний exact match; `null` — ключ обов'язковий, значення
2290
+ * довільне (T0 проставляє дефолт `'true'`, якщо ключ відсутній).
2291
+ */
2292
+ export const HASURA_REQUIRED_ENV_VALUES = {
2293
+ HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: 'true',
2294
+ HASURA_GRAPHQL_ENABLE_RELAY: 'false',
2295
+ HASURA_GRAPHQL_ENABLE_TELEMETRY: 'false',
2296
+ HASURA_GRAPHQL_ENABLED_LOG_TYPES: 'startup,http-log',
2297
+ HASURA_GRAPHQL_ENABLED_APIS: 'metadata,graphql,pgdump',
2298
+ HASURA_GRAPHQL_DISABLE_EVENTING: null
2299
+ }
2300
+
2286
2301
  const K8S_YAML_EXT_RE = /\.ya?ml$/iu
2287
2302
 
2288
2303
  /**
@@ -2720,7 +2735,7 @@ function hasuraRuleMatchesSinglePathNoHeaders(rule, pathType, pathValue) {
2720
2735
  * @param {string} toPath очікуваний `requestRedirect.path.replaceFullPath`
2721
2736
  * @returns {boolean} true — якщо filters відповідають канону редиректу
2722
2737
  */
2723
- function hasuraRuleHasExactRedirect(rule, toPath) {
2738
+ export function hasuraRuleHasExactRedirect(rule, toPath) {
2724
2739
  const r = asPlainRecord(rule)
2725
2740
  if (r === null) return false
2726
2741
  const filters = r.filters
@@ -2830,7 +2845,7 @@ function hasuraRuleIsWebsocket(rule, qlPath) {
2830
2845
  * @param {unknown[]} rules вміст `spec.rules` HTTPRoute
2831
2846
  * @returns {{ prefix: string, startIndex: number } | null} виявлений префікс і позиція правила 1 або null
2832
2847
  */
2833
- function findHasuraCanonStart(rules) {
2848
+ export function findHasuraCanonStart(rules) {
2834
2849
  for (const [i, rule] of rules.entries()) {
2835
2850
  const r = asPlainRecord(rule)
2836
2851
  const matches = r === null ? null : r.matches
@@ -3166,7 +3181,7 @@ function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
3166
3181
  * звіряє канон 4 правил (див. `httpRouteHasuraCanonViolation` і k8s.mdc).
3167
3182
  * @param {string} root корінь репозиторію
3168
3183
  * @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
3169
- * @param {(msg: string) => void} fail callback реєстрації помилки
3184
+ * @param {(msg: string, opts?: string | { reason?: string, file?: string, data?: object }) => void} fail callback реєстрації помилки
3170
3185
  * @returns {Promise<void>} результат
3171
3186
  */
3172
3187
  async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
@@ -3193,7 +3208,12 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
3193
3208
  })
3194
3209
  for (const v of violations) {
3195
3210
  const rel = (relative(root, v.filename) || v.filename).replaceAll('\\', '/')
3196
- fail(`${rel}: ${v.message}`)
3211
+ // T0 автофіксить лише "rule1_filters" (детермінований overwrite існуючого правила);
3212
+ // rule2/3/4-missing вимагають синтезу нового правила з backendRef — не T0 (k8s.mdc).
3213
+ const hint = REGO_HINT_HASURA_HTTPROUTE_RULE1_RE.test(v.message)
3214
+ ? { reason: 'hasura-httproute-rule1-filters', file: rel, data: { kind: 'hasura-httproute-rule1-filters' } }
3215
+ : undefined
3216
+ fail(`${rel}: ${v.message}`, hint)
3197
3217
  }
3198
3218
  }
3199
3219
 
@@ -3613,7 +3633,7 @@ async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs
3613
3633
  })
3614
3634
  for (const v of violations) {
3615
3635
  const rel = (relative(root, v.filename) || v.filename).replaceAll('\\', '/')
3616
- fail(`${rel}: ${v.message}`)
3636
+ fail(`${rel}: ${v.message}`, { reason: 'hasura-configmap-env', file: rel, data: { kind: 'hasura-configmap-env' } })
3617
3637
  }
3618
3638
  if (violations.length === 0) {
3619
3639
  passFn(
@@ -6466,6 +6486,10 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs, fail) {
6466
6486
  const REGO_HINT_DEPLOYMENT_STRATEGY_RE = /spec\.strategy має бути RollingUpdate/u
6467
6487
  const REGO_HINT_NETWORKPOLICY_EGRESS_RE = /відсутнє обовʼязкове egress-правило/u
6468
6488
  const REGO_HINT_KUSTOMIZATION_PATCHES_RE = /patches має бути за алфавітом/u
6489
+ /** Стадія `rule1_filters` `k8s.hasura_httproute` — єдина автофіксовна (overwrite існуючого правила, без синтезу нового). */
6490
+ const REGO_HINT_HASURA_HTTPROUTE_RULE1_RE = /правило 1 Hasura-канона \(rules\[\d+\]/u
6491
+ const REGO_HINT_SVC_CLUSTERIP_RE = /spec\.type[^\n]*ClusterIP|додай spec\.type: ClusterIP/u
6492
+ const REGO_HINT_SVC_HL_CLUSTERIP_RE = /spec\.clusterIP[^\n]*None|додай spec\.clusterIP: None/u
6469
6493
  /**
6470
6494
  * Structured fix-hint (#3) для rego-violation із `runAllK8sRego` — щоб T0
6471
6495
  * (`fix-manifests.mjs`) автофіксив детерміновано, не парсячи message. Класифікація
@@ -6486,6 +6510,12 @@ function k8sRegoFixHint(ns, file, message) {
6486
6510
  if (ns === 'k8s.kustomization' && REGO_HINT_KUSTOMIZATION_PATCHES_RE.test(m)) {
6487
6511
  return { reason: 'kustomization-patches-sort', file, data: { kind: 'kustomization-patches-sort' } }
6488
6512
  }
6513
+ if (ns === 'k8s.svc_yaml' && REGO_HINT_SVC_CLUSTERIP_RE.test(m)) {
6514
+ return { reason: 'svc-clusterip-type', file, data: { kind: 'svc-clusterip-type' } }
6515
+ }
6516
+ if (ns === 'k8s.svc_hl_yaml' && REGO_HINT_SVC_HL_CLUSTERIP_RE.test(m)) {
6517
+ return { reason: 'svc-hl-cluster-ip', file, data: { kind: 'svc-hl-cluster-ip' } }
6518
+ }
6489
6519
  // немає збігу — implicit undefined (без trailing return: no-redundant-jump + no-useless-undefined тихі)
6490
6520
  }
6491
6521