@7n/rules 1.7.6 → 1.8.1
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 +12 -0
- package/package.json +1 -1
- package/rules/js/check/docs/fix-check.md +32 -15
- package/rules/js/check/docs/main.md +5 -2
- package/rules/js/check/fix-check.mjs +55 -12
- package/rules/js/check/main.mjs +4 -2
- package/rules/js/tooling/docs/main.md +31 -9
- package/rules/js/tooling/main.mjs +34 -0
- package/rules/k8s/manifests/fix-manifests.mjs +195 -1
- package/rules/k8s/manifests/main.mjs +35 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.1] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- docs(adr): нормалізувати чернетки ADR у канонічний MADR-формат
|
|
8
|
+
|
|
9
|
+
## [1.8.0] - 2026-07-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- js/check: T0-фікс для .oxlintrc.json (копіювання/merge канону без LLM-ладдер)
|
|
14
|
+
|
|
3
15
|
## [1.7.6] - 2026-07-16
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
|
@@ -3,29 +3,46 @@ type: JS Module
|
|
|
3
3
|
title: fix-check.mjs
|
|
4
4
|
resource: npm/rules/js/check/fix-check.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 48182147
|
|
7
7
|
model: manual
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## Огляд
|
|
11
11
|
|
|
12
|
-
T0-autofix для `js/check`:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
|
|
12
|
+
T0-autofix для `js/check`: два детерміновані патерни, обидва без LLM.
|
|
13
|
+
|
|
14
|
+
- `js-check-eslint-config` — scaffold/merge `eslint.config.js` за планом
|
|
15
|
+
`planEslintConfigFix` (детекція воркспейс-типів node/vue). Раніше ці
|
|
16
|
+
порушення йшли у LLM-ладдер, який переписував конфіг цілком і вгадував типи
|
|
17
|
+
(інцидент: у vue-монорепо записано `getConfig({ node: ['npm'] })` — eslint
|
|
18
|
+
перестав обробляти .vue файли).
|
|
19
|
+
- `js-check-oxlintrc` — scaffold/merge `.oxlintrc.json` за планом
|
|
20
|
+
`planOxlintrcFix` (`../tooling/main.mjs`). Раніше ці порушення теж йшли у
|
|
21
|
+
LLM-ладдер: 15 КБ канону дешева модель не відтворює byte-perfect (verify
|
|
22
|
+
fail), а дорожча не встигає за один rung-таймаут.
|
|
17
23
|
|
|
18
24
|
## Поведінка
|
|
19
25
|
|
|
20
|
-
-
|
|
21
|
-
`eslint-config-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
26
|
+
- `js-check-eslint-config` тригериться на reason-и `eslint-config-missing` /
|
|
27
|
+
`eslint-config-ignores` / `eslint-config-vue-workspace`. `apply` перераховує
|
|
28
|
+
план із поточного стану диска (ідемпотентно): відсутній конфіг —
|
|
29
|
+
створюється з детектованими типами; наявний — зазнає хірургічного merge, без
|
|
30
|
+
повного перезапису. Якщо план порожній (`null`) — жодних змін
|
|
31
|
+
(`touchedFiles: []`).
|
|
32
|
+
- `js-check-oxlintrc` тригериться на reason-и `oxlintrc-missing` /
|
|
33
|
+
`oxlintrc-drift` (`OXLINTRC_MISSING` / `OXLINTRC_DRIFT` з `../tooling/main.mjs`).
|
|
34
|
+
`apply` читає наявний `.oxlintrc.json` (`null`, якщо відсутній або невалідний
|
|
35
|
+
JSON) і канон, будує злитий обʼєкт через `planOxlintrcFix` і перезаписує
|
|
36
|
+
файл цілком (JSON, 2-space, з кінцевим переносом рядка).
|
|
37
|
+
- Решта порушень `js/check` (engines, workflows) — поза цим T0, стандартний
|
|
38
|
+
шлях (ladder/manual).
|
|
39
|
+
- Обидва патерни викликають `ctx.recordWrite` перед записом (pre-image для
|
|
40
|
+
central rollback).
|
|
27
41
|
|
|
28
42
|
## Гарантії поведінки
|
|
29
43
|
|
|
30
|
-
-
|
|
31
|
-
|
|
44
|
+
- `js-check-eslint-config` записує лише `eslint.config.js`/`eslint.config.mjs`
|
|
45
|
+
у корені репо; наявний конфіг ніколи не перезаписується цілком — тільки
|
|
46
|
+
точкові вставки/заміни.
|
|
47
|
+
- `js-check-oxlintrc` записує лише `.oxlintrc.json` у корені репо; результат
|
|
48
|
+
завжди проходить `verifyOxlintRcAgainstCanonical` без ручного втручання.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/js/check/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 128c6700
|
|
7
7
|
model: manual
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -27,7 +27,10 @@ Detector concern-а `js/check`: read-only перевірка відповідн
|
|
|
27
27
|
`engines.bun >= 1.3` (кореневий `package.json` валідує Rego
|
|
28
28
|
`npm/policy/js_lint/package_json/`).
|
|
29
29
|
- `.oxlintrc.json`: існує, валідний JSON, збігається з каноном oxlint із пакета
|
|
30
|
-
`@7n/rules
|
|
30
|
+
`@7n/rules` (`verifyOxlintRcAgainstCanonical` з `../tooling/main.mjs`).
|
|
31
|
+
Відсутність — reason `oxlintrc-missing`, розходження з каноном — reason
|
|
32
|
+
`oxlintrc-drift` (константи `OXLINTRC_MISSING`/`OXLINTRC_DRIFT`); обидва
|
|
33
|
+
детерміновано виправляє T0 `fix-check.mjs` (патерн `js-check-oxlintrc`).
|
|
31
34
|
- `.github/workflows/lint-js.yml`: існує; `lint.yml` (якщо є) не дублює
|
|
32
35
|
oxlint/eslint/jscpd кроки.
|
|
33
36
|
- `knip.json`: якщо відсутній — копіюється канонічний baseline із пакета
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
/** @see ./docs/fix-check.md */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* T0-autofix для `js/check` — детермінований scaffold/merge `eslint.config.js
|
|
5
|
-
* Раніше
|
|
6
|
-
*
|
|
7
|
-
* `getConfig({ node: ['npm'] })` — eslint перестав
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* T0-autofix для `js/check` — детермінований scaffold/merge `eslint.config.js`
|
|
5
|
+
* і `.oxlintrc.json`. Раніше обидва йшли у LLM-ладдер, який переписував конфіги
|
|
6
|
+
* цілком: для eslint.config.js це вгадувало воркспейс-типи (інцидент: у
|
|
7
|
+
* vue-монорепо записано `getConfig({ node: ['npm'] })` — eslint перестав
|
|
8
|
+
* обробляти .vue файли), а для .oxlintrc.json — 15 КБ канону, який дешевій
|
|
9
|
+
* моделі не відтворити byte-perfect (verify fail), а дорожчій — не встигнути
|
|
10
|
+
* за один rung-таймаут.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Джерело правди для eslint.config.js — `planEslintConfigFix` (детекція типів
|
|
13
|
+
* із workspaces root package.json + vue-залежність/.vue-файли), наявний конфіг
|
|
14
|
+
* оновлюється точковим merge хірургічно, без повного перезапису.
|
|
15
|
+
*
|
|
16
|
+
* Джерело правди для .oxlintrc.json — `planOxlintrcFix`, що дзеркалить
|
|
17
|
+
* `verifyOxlintRcAgainstCanonical`: відсутній файл копіює канон, наявний —
|
|
18
|
+
* доповнюється до канону без втрати project-specific розширень (зайві
|
|
19
|
+
* `rules`-ключі й `ignorePatterns` зберігаються).
|
|
20
|
+
*
|
|
21
|
+
* Решта порушень js/check (engines, workflows) — поза цим T0, стандартний шлях
|
|
22
|
+
* (ladder/manual).
|
|
14
23
|
*/
|
|
15
|
-
import {
|
|
24
|
+
import { existsSync } from 'node:fs'
|
|
25
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
26
|
+
import { join } from 'node:path'
|
|
16
27
|
|
|
17
28
|
import {
|
|
18
29
|
ESLINT_CONFIG_IGNORES,
|
|
@@ -20,14 +31,31 @@ import {
|
|
|
20
31
|
ESLINT_CONFIG_VUE_WORKSPACE,
|
|
21
32
|
planEslintConfigFix
|
|
22
33
|
} from './eslint-config.mjs'
|
|
34
|
+
import { OXLINT_CANONICAL_JSON_PATH, OXLINTRC_DRIFT, OXLINTRC_MISSING, planOxlintrcFix } from '../tooling/main.mjs'
|
|
23
35
|
|
|
24
|
-
const
|
|
36
|
+
const ESLINT_CONFIG_REASONS = new Set([ESLINT_CONFIG_MISSING, ESLINT_CONFIG_IGNORES, ESLINT_CONFIG_VUE_WORKSPACE])
|
|
37
|
+
const OXLINTRC_REASONS = new Set([OXLINTRC_MISSING, OXLINTRC_DRIFT])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Читає `.oxlintrc.json`, якщо є й валідний JSON; інакше `null` (трактується
|
|
41
|
+
* як відсутній файл — `planOxlintrcFix` будує з канону).
|
|
42
|
+
* @param {string} oxPath абсолютний шлях до `.oxlintrc.json`
|
|
43
|
+
* @returns {Promise<unknown>} розпарсений вміст або `null`
|
|
44
|
+
*/
|
|
45
|
+
async function readOxlintrcOrNull(oxPath) {
|
|
46
|
+
if (!existsSync(oxPath)) return null
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(oxPath, 'utf8'))
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
25
53
|
|
|
26
54
|
/** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
27
55
|
export const patterns = [
|
|
28
56
|
{
|
|
29
57
|
id: 'js-check-eslint-config',
|
|
30
|
-
test: violations => violations.some(v =>
|
|
58
|
+
test: violations => violations.some(v => ESLINT_CONFIG_REASONS.has(v.reason)),
|
|
31
59
|
apply: async (violations, ctx) => {
|
|
32
60
|
const plan = await planEslintConfigFix(ctx.cwd)
|
|
33
61
|
if (plan === null) return { touchedFiles: [] }
|
|
@@ -35,5 +63,20 @@ export const patterns = [
|
|
|
35
63
|
await writeFile(plan.path, plan.content, 'utf8')
|
|
36
64
|
return { touchedFiles: [plan.path], message: plan.message }
|
|
37
65
|
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'js-check-oxlintrc',
|
|
69
|
+
test: violations => violations.some(v => OXLINTRC_REASONS.has(v.reason)),
|
|
70
|
+
apply: async (violations, ctx) => {
|
|
71
|
+
const oxPath = join(ctx.cwd, '.oxlintrc.json')
|
|
72
|
+
const [actual, canonicalRaw] = await Promise.all([
|
|
73
|
+
readOxlintrcOrNull(oxPath),
|
|
74
|
+
readFile(OXLINT_CANONICAL_JSON_PATH, 'utf8')
|
|
75
|
+
])
|
|
76
|
+
const merged = planOxlintrcFix(actual, JSON.parse(canonicalRaw))
|
|
77
|
+
ctx.recordWrite?.(oxPath)
|
|
78
|
+
await writeFile(oxPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
|
|
79
|
+
return { touchedFiles: [oxPath], message: '.oxlintrc.json: T0 merge до канону oxlint (@7n/rules)' }
|
|
80
|
+
}
|
|
38
81
|
}
|
|
39
82
|
]
|
package/rules/js/check/main.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { createViolationReporter } from '../../../scripts/lib/lint-surface/viola
|
|
|
8
8
|
import {
|
|
9
9
|
KNIP_CANONICAL_JSON_PATH,
|
|
10
10
|
OXLINT_CANONICAL_JSON_PATH,
|
|
11
|
+
OXLINTRC_DRIFT,
|
|
12
|
+
OXLINTRC_MISSING,
|
|
11
13
|
verifyOxlintRcAgainstCanonical
|
|
12
14
|
} from '../tooling/main.mjs'
|
|
13
15
|
|
|
@@ -206,7 +208,7 @@ async function checkPackageJsonJsLint(passFn, failFn, cwd) {
|
|
|
206
208
|
async function checkOxlintRc(passFn, failFn, cwd) {
|
|
207
209
|
const oxPath = join(cwd, '.oxlintrc.json')
|
|
208
210
|
if (!existsSync(oxPath)) {
|
|
209
|
-
failFn('.oxlintrc.json не існує — додай конфіг oxlint (js.mdc)')
|
|
211
|
+
failFn('.oxlintrc.json не існує — додай конфіг oxlint (js.mdc)', OXLINTRC_MISSING)
|
|
210
212
|
return
|
|
211
213
|
}
|
|
212
214
|
let oxCfg
|
|
@@ -229,7 +231,7 @@ async function checkOxlintRc(passFn, failFn, cwd) {
|
|
|
229
231
|
passFn('.oxlintrc.json збігається з каноном oxlint (@7n/rules)')
|
|
230
232
|
} else {
|
|
231
233
|
for (const msg of oxV.failures) {
|
|
232
|
-
failFn(msg)
|
|
234
|
+
failFn(msg, OXLINTRC_DRIFT)
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
}
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/js/tooling/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 76437b89
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.98
|
|
@@ -12,20 +12,42 @@ docgen:
|
|
|
12
12
|
|
|
13
13
|
## Огляд
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Шляхи до канонічних JSON-конфігурацій для `oxlint` та `knip` доступні через
|
|
16
|
+
`OXLINT_CANONICAL_JSON_PATH` та `KNIP_CANONICAL_JSON_PATH`. Модуль перевіряє,
|
|
17
|
+
чи відповідає `.oxlintrc.json` канону (`verifyOxlintRcAgainstCanonical`), і
|
|
18
|
+
будує детермінований merge до канону (`planOxlintrcFix`) — джерело правди для
|
|
19
|
+
T0-автофіксу `js/check` (`fix-check.mjs`), без LLM.
|
|
16
20
|
|
|
17
21
|
## Поведінка
|
|
18
22
|
|
|
19
|
-
OXLINT_CANONICAL_JSON_PATH —
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
- `OXLINT_CANONICAL_JSON_PATH` / `KNIP_CANONICAL_JSON_PATH` — шляхи до
|
|
24
|
+
канонічних JSON-конфігів `oxlint`/`knip` у цьому пакеті.
|
|
25
|
+
- `OXLINTRC_MISSING` / `OXLINTRC_DRIFT` — стабільні reason-коди для
|
|
26
|
+
порушень `.oxlintrc.json` (відсутній файл / розходження з каноном),
|
|
27
|
+
які `js/check` проставляє через `createViolationReporter`, а T0-патерн
|
|
28
|
+
`js-check-oxlintrc` розпізнає для автофіксу.
|
|
29
|
+
- `verifyOxlintRcAgainstCanonical(cfg, canonical)` — звіряє `.oxlintrc.json`
|
|
30
|
+
проти канону: усі `rules`-ключі канону мають точний збіг значення (зайві
|
|
31
|
+
локальні ключі дозволені), `ignorePatterns` канону мають бути присутні
|
|
32
|
+
(локальні розширення дозволені), решта полів — точний глибокий збіг.
|
|
33
|
+
- `planOxlintrcFix(actual, canonical)` — чиста функція, що дзеркалить
|
|
34
|
+
правила `verifyOxlintRcAgainstCanonical` у зворотний бік: будує обʼєкт
|
|
35
|
+
`.oxlintrc.json`, який гарантовано проходить повторну перевірку.
|
|
36
|
+
Відсутній `actual` (`null`) трактується як порожній файл — результат
|
|
37
|
+
дорівнює канону. Наявний `actual` доповнюється до канону без втрати
|
|
38
|
+
project-specific розширень: зайві `rules`-ключі й `ignorePatterns`
|
|
39
|
+
зберігаються, а канонічні `rules`-значення й поля верхнього рівня
|
|
40
|
+
перезаписуються канонічними (єдине валідне значення для перевірки).
|
|
22
41
|
|
|
23
42
|
## Публічний API
|
|
24
43
|
|
|
25
|
-
- OXLINT_CANONICAL_JSON_PATH —
|
|
26
|
-
- KNIP_CANONICAL_JSON_PATH —
|
|
27
|
-
-
|
|
44
|
+
- `OXLINT_CANONICAL_JSON_PATH` — шлях до канонічного конфігу `oxlint` для перевірки/T0-фіксу.
|
|
45
|
+
- `KNIP_CANONICAL_JSON_PATH` — шлях до канонічного конфігу `knip`, що копіюється у корінь проєкту-споживача, якщо відсутній.
|
|
46
|
+
- `OXLINTRC_MISSING` / `OXLINTRC_DRIFT` — reason-коди порушень `.oxlintrc.json`.
|
|
47
|
+
- `verifyOxlintRcAgainstCanonical(cfg, canonical)` → `{ ok, failures }` — перевірка `.oxlintrc.json` проти канону.
|
|
48
|
+
- `planOxlintrcFix(actual, canonical)` → злитий обʼєкт `.oxlintrc.json` — детермінований T0-фікс без LLM.
|
|
28
49
|
|
|
29
50
|
## Гарантії поведінки
|
|
30
51
|
|
|
31
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
52
|
+
- Read-only: не виконує операцій запису (ФС/БД); запис у `.oxlintrc.json` — відповідальність виклику (T0 `fix-check.mjs`).
|
|
53
|
+
- `planOxlintrcFix` — чиста функція без side effects; той самий вхід завжди дає той самий вихід.
|
|
@@ -18,6 +18,11 @@ export const KNIP_CANONICAL_JSON_PATH = join(
|
|
|
18
18
|
'knip-canonical.json'
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
/** `.oxlintrc.json` відсутній — T0 копіює канон (`fix-check.mjs`). */
|
|
22
|
+
export const OXLINTRC_MISSING = 'oxlintrc-missing'
|
|
23
|
+
/** `.oxlintrc.json` існує, але розходиться з каноном — T0 доводить до відповідності детермінованим merge. */
|
|
24
|
+
export const OXLINTRC_DRIFT = 'oxlintrc-drift'
|
|
25
|
+
|
|
21
26
|
// Канонічний рядок `lint-js`-скрипта і мінімальна версія `@nitra/eslint-config` —
|
|
22
27
|
// у rego (`npm/policy/js_lint/package_json/`). JS-копії (`CANONICAL_LINT_JS`,
|
|
23
28
|
// `isCanonicalLintJs`, `nitraEslintConfigMeetsMinVersion`) видалено, щоб не
|
|
@@ -147,3 +152,32 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
|
|
|
147
152
|
|
|
148
153
|
return { ok: failures.length === 0, failures }
|
|
149
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Детермінований merge `.oxlintrc.json` до відповідності канону (T0 `fix-check.mjs`) —
|
|
158
|
+
* дзеркалить правила `verifyOxlintRcAgainstCanonical`, тож результат завжди проходить
|
|
159
|
+
* повторну перевірку без LLM. Project-specific розширення зберігаються: зайві ключі в
|
|
160
|
+
* `rules` і зайві `ignorePatterns` не видаляються, лише доповнюються канонічними.
|
|
161
|
+
* @param {unknown} actual поточний вміст `.oxlintrc.json` (`null`/будь-що не-object — трактується як відсутній файл)
|
|
162
|
+
* @param {Record<string, unknown>} canonical розпарсений `oxlint-canonical.json`
|
|
163
|
+
* @returns {Record<string, unknown>} злитий обʼєкт, готовий до запису у `.oxlintrc.json`
|
|
164
|
+
*/
|
|
165
|
+
export function planOxlintrcFix(actual, canonical) {
|
|
166
|
+
const merged = { ...asRecordOrEmpty(actual) }
|
|
167
|
+
for (const [key, expected] of Object.entries(canonical)) {
|
|
168
|
+
if (key === 'rules') {
|
|
169
|
+
merged.rules = { ...asRecordOrEmpty(merged.rules), ...asRecordOrEmpty(expected) }
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (key === 'ignorePatterns') {
|
|
173
|
+
const existing = Array.isArray(merged.ignorePatterns) ? merged.ignorePatterns : []
|
|
174
|
+
const canonPatterns = Array.isArray(expected) ? expected : []
|
|
175
|
+
merged.ignorePatterns = [...existing, ...canonPatterns.filter(p => !existing.includes(p))]
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
// Інші поля вимагають точного збігу з каноном (verifyOxlintRcAgainstCanonical) —
|
|
179
|
+
// єдине валідне значення й так канонічне, тож перезапис безпечний.
|
|
180
|
+
merged[key] = expected
|
|
181
|
+
}
|
|
182
|
+
return merged
|
|
183
|
+
}
|
|
@@ -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,167 @@ 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_*` env-ключі (`HASURA_REQUIRED_ENV_VALUES`,
|
|
162
|
+
* дзеркалить `k8s.hasura_configmap.rego`) у `data` документа `kind: ConfigMap`. Ключ з
|
|
163
|
+
* очікуванням `null` (наприклад `HASURA_GRAPHQL_DISABLE_EVENTING`) — довільне значення,
|
|
164
|
+
* T0 проставляє лише якщо ключ відсутній (дефолт `'true'`).
|
|
165
|
+
* @param {string} content вміст configmap.yaml
|
|
166
|
+
* @returns {string|null} новий вміст або null, якщо змін немає / парс невпевнений
|
|
167
|
+
*/
|
|
168
|
+
export function ensureHasuraConfigMapRequiredEnv(content) {
|
|
169
|
+
let doc
|
|
170
|
+
try {
|
|
171
|
+
doc = parseDocument(content)
|
|
172
|
+
} catch {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
if (doc.errors?.length) return null
|
|
176
|
+
if (doc.get('kind') !== 'ConfigMap') return null
|
|
177
|
+
|
|
178
|
+
let changed = false
|
|
179
|
+
for (const [key, expected] of Object.entries(HASURA_REQUIRED_ENV_VALUES)) {
|
|
180
|
+
const current = doc.getIn(['data', key])
|
|
181
|
+
if (expected === null) {
|
|
182
|
+
if (current === undefined) {
|
|
183
|
+
doc.setIn(['data', key], 'true')
|
|
184
|
+
changed = true
|
|
185
|
+
}
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (expected === 'true') {
|
|
189
|
+
if (!isTruthyBool(current)) {
|
|
190
|
+
doc.setIn(['data', key], 'true')
|
|
191
|
+
changed = true
|
|
192
|
+
}
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (expected === 'false') {
|
|
196
|
+
if (!isFalsyBool(current)) {
|
|
197
|
+
doc.setIn(['data', key], 'false')
|
|
198
|
+
changed = true
|
|
199
|
+
}
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (current !== expected) {
|
|
203
|
+
doc.setIn(['data', key], expected)
|
|
204
|
+
changed = true
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!changed) return null
|
|
208
|
+
return doc.toString()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Проставляє канонічний RequestRedirect (правило 1 Hasura-канона, `k8s.hasura_httproute.rego`)
|
|
213
|
+
* у вже знайдене правило `Exact "<prefix>/ql"`. Фіксує лише **існуюче** правило (overwrite
|
|
214
|
+
* `filters`) — правила 2-4 (rule2/3/4_missing) потребують синтезу нового правила з
|
|
215
|
+
* `backendRef`, якого нізвідки достовірно вивести, тож НЕ T0 (за тим самим принципом, що й
|
|
216
|
+
* `internal-url-invalid` у `hasura/internal_urls`: людське рішення про інфраструктуру).
|
|
217
|
+
* @param {string} content вміст hr.yaml (HTTPRoute, може бути multi-doc)
|
|
218
|
+
* @returns {string|null} новий вміст або null, якщо змін немає / не застосовно / парс невпевнений
|
|
219
|
+
*/
|
|
220
|
+
export function ensureHasuraHttpRouteRule1Filters(content) {
|
|
221
|
+
let docs
|
|
222
|
+
try {
|
|
223
|
+
docs = parseAllDocuments(content)
|
|
224
|
+
} catch {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
if (docs.some(d => d.errors?.length)) return null
|
|
228
|
+
let changed = false
|
|
229
|
+
for (const doc of docs) {
|
|
230
|
+
if (doc.get('kind') !== 'HTTPRoute') continue
|
|
231
|
+
const plainRules = doc.toJSON()?.spec?.rules
|
|
232
|
+
if (!Array.isArray(plainRules) || plainRules.length === 0) continue
|
|
233
|
+
const start = findHasuraCanonStart(plainRules)
|
|
234
|
+
if (start === null) continue
|
|
235
|
+
const consolePath = `${start.prefix}/ql/console`
|
|
236
|
+
if (hasuraRuleHasExactRedirect(plainRules[start.startIndex], consolePath)) continue // вже канонічно
|
|
237
|
+
const canonicalFilters = [
|
|
238
|
+
{
|
|
239
|
+
type: 'RequestRedirect',
|
|
240
|
+
requestRedirect: { statusCode: 302, path: { type: 'ReplaceFullPath', replaceFullPath: consolePath } }
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
doc.setIn(['spec', 'rules', start.startIndex, 'filters'], doc.createNode(canonicalFilters))
|
|
244
|
+
changed = true
|
|
245
|
+
}
|
|
246
|
+
if (!changed) return null
|
|
247
|
+
return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Проставляє `spec.type: ClusterIP` у документ `kind: Service` (`svc.yaml`, `k8s.svc_yaml.rego`).
|
|
252
|
+
* @param {string} content вміст svc.yaml (може бути multi-doc)
|
|
253
|
+
* @returns {string|null} новий вміст або null, якщо змін немає / не застосовно / парс невпевнений
|
|
254
|
+
*/
|
|
255
|
+
export function ensureSvcClusterIpType(content) {
|
|
256
|
+
let docs
|
|
257
|
+
try {
|
|
258
|
+
docs = parseAllDocuments(content)
|
|
259
|
+
} catch {
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
if (docs.some(d => d.errors?.length)) return null
|
|
263
|
+
let changed = false
|
|
264
|
+
for (const doc of docs) {
|
|
265
|
+
if (doc.get('kind') !== 'Service') continue
|
|
266
|
+
if (doc.getIn(['spec', 'type']) === 'ClusterIP') continue
|
|
267
|
+
doc.setIn(['spec', 'type'], 'ClusterIP')
|
|
268
|
+
changed = true
|
|
269
|
+
}
|
|
270
|
+
if (!changed) return null
|
|
271
|
+
return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Проставляє `spec.clusterIP: None` у документ `kind: Service` (`svc-hl.yaml`, `k8s.svc_hl_yaml.rego`).
|
|
276
|
+
* Не чіпає `metadata.name` (суфікс `-hl`) — перейменування ресурсу впливає на посилання з
|
|
277
|
+
* інших файлів (ConfigMap/Deployment/HTTPRoute), тож НЕ T0.
|
|
278
|
+
* @param {string} content вміст svc-hl.yaml (може бути multi-doc)
|
|
279
|
+
* @returns {string|null} новий вміст або null, якщо змін немає / не застосовно / парс невпевнений
|
|
280
|
+
*/
|
|
281
|
+
export function ensureSvcHlClusterIp(content) {
|
|
282
|
+
let docs
|
|
283
|
+
try {
|
|
284
|
+
docs = parseAllDocuments(content)
|
|
285
|
+
} catch {
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
if (docs.some(d => d.errors?.length)) return null
|
|
289
|
+
let changed = false
|
|
290
|
+
for (const doc of docs) {
|
|
291
|
+
if (doc.get('kind') !== 'Service') continue
|
|
292
|
+
if (doc.getIn(['spec', 'clusterIP']) === 'None') continue
|
|
293
|
+
doc.setIn(['spec', 'clusterIP'], 'None')
|
|
294
|
+
changed = true
|
|
295
|
+
}
|
|
296
|
+
if (!changed) return null
|
|
297
|
+
return docs.map(d => d.toString().replace(LEADING_DOC_SEPARATOR_RE, '').trimEnd()).join('\n---\n') + '\n'
|
|
298
|
+
}
|
|
299
|
+
|
|
130
300
|
/**
|
|
131
301
|
* Застосовує текстовий трансформер до унікальних файлів із targets і пише зміни.
|
|
132
302
|
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintViolation[]} targets violations із файлами для правки
|
|
@@ -224,5 +394,29 @@ export const patterns = [
|
|
|
224
394
|
'networkpolicy-egress',
|
|
225
395
|
ensureNetworkPolicyEgress,
|
|
226
396
|
n => `канонічний spec.egress проставлено: ${n} файл(ів)`
|
|
397
|
+
),
|
|
398
|
+
fileTransformPattern(
|
|
399
|
+
'k8s-manifests-hasura-configmap-env',
|
|
400
|
+
'hasura-configmap-env',
|
|
401
|
+
ensureHasuraConfigMapRequiredEnv,
|
|
402
|
+
n => `обов'язкові HASURA_GRAPHQL_* env проставлено: ${n} файл(ів)`
|
|
403
|
+
),
|
|
404
|
+
fileTransformPattern(
|
|
405
|
+
'k8s-manifests-hasura-httproute-rule1-filters',
|
|
406
|
+
'hasura-httproute-rule1-filters',
|
|
407
|
+
ensureHasuraHttpRouteRule1Filters,
|
|
408
|
+
n => `правило 1 Hasura-канона (RequestRedirect) проставлено: ${n} файл(ів)`
|
|
409
|
+
),
|
|
410
|
+
fileTransformPattern(
|
|
411
|
+
'k8s-manifests-svc-clusterip-type',
|
|
412
|
+
'svc-clusterip-type',
|
|
413
|
+
ensureSvcClusterIpType,
|
|
414
|
+
n => `svc.yaml spec.type: ClusterIP проставлено: ${n} файл(ів)`
|
|
415
|
+
),
|
|
416
|
+
fileTransformPattern(
|
|
417
|
+
'k8s-manifests-svc-hl-cluster-ip',
|
|
418
|
+
'svc-hl-cluster-ip',
|
|
419
|
+
ensureSvcHlClusterIp,
|
|
420
|
+
n => `svc-hl.yaml spec.clusterIP: None проставлено: ${n} файл(ів)`
|
|
227
421
|
)
|
|
228
422
|
]
|
|
@@ -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
|
-
|
|
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
|
|