@7n/rules 1.8.2 → 1.8.4
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.4] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- text/run-v8r: включати причину провалу v8r (stdout+stderr, без noise) у violation-повідомлення замість голого коду виходу
|
|
8
|
+
- k8s/manifests: форматування tests/check-schema.test.mjs (oxfmt)
|
|
9
|
+
|
|
10
|
+
## [1.8.3] - 2026-07-17
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- release: @7n/rules@1.8.2
|
|
15
|
+
|
|
3
16
|
## [1.8.2] - 2026-07-17
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ Rego-пакет: `k8s.hasura_configmap`
|
|
|
11
11
|
| `HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS` | `"true"` |
|
|
12
12
|
| `HASURA_GRAPHQL_ENABLE_RELAY` | `"false"` |
|
|
13
13
|
| `HASURA_GRAPHQL_ENABLE_TELEMETRY` | `"false"` |
|
|
14
|
-
| `HASURA_GRAPHQL_ENABLED_LOG_TYPES` | `"startup,http-log"` (
|
|
14
|
+
| `HASURA_GRAPHQL_ENABLED_LOG_TYPES` | `"startup,http-log"` (точно; base/dev; overlay має патчем замінити на `"startup"` — `http-log` лише для base/dev) |
|
|
15
15
|
| `HASURA_GRAPHQL_ENABLED_APIS` | `"metadata,graphql,pgdump"` (base/dev; overlay має патчем замінити на `"metadata,graphql"`) |
|
|
16
16
|
| `HASURA_GRAPHQL_DISABLE_EVENTING` | ключ обов'язковий, значення довільне (рекомендовано `"true"`) |
|
|
17
17
|
|
|
@@ -4922,6 +4922,12 @@ const HASURA_OVERLAY_ENABLED_APIS = 'metadata,graphql'
|
|
|
4922
4922
|
/** JSON-Pointer ключа `HASURA_GRAPHQL_ENABLED_APIS` у `data` ConfigMap (для JSON6902-патчів). */
|
|
4923
4923
|
const HASURA_ENABLED_APIS_DATA_POINTER = '/data/HASURA_GRAPHQL_ENABLED_APIS'
|
|
4924
4924
|
|
|
4925
|
+
/** Очікуване `HASURA_GRAPHQL_ENABLED_LOG_TYPES` у non-base/dev overlay (без `http-log` — він лише для base/dev). */
|
|
4926
|
+
const HASURA_OVERLAY_ENABLED_LOG_TYPES = 'startup'
|
|
4927
|
+
|
|
4928
|
+
/** JSON-Pointer ключа `HASURA_GRAPHQL_ENABLED_LOG_TYPES` у `data` ConfigMap (для JSON6902-патчів). */
|
|
4929
|
+
const HASURA_ENABLED_LOG_TYPES_DATA_POINTER = '/data/HASURA_GRAPHQL_ENABLED_LOG_TYPES'
|
|
4930
|
+
|
|
4925
4931
|
/**
|
|
4926
4932
|
* Чи дерево kustomization (`resources` / `bases` / `components` / `crds`, рекурсивно) містить
|
|
4927
4933
|
* **Hasura-Deployment** у шарі base (образ `hasura/graphql-engine`). Маркер того, що overlay успадковує
|
|
@@ -4965,18 +4971,19 @@ function firstValidYamlJsonFromPatchText(patchText) {
|
|
|
4965
4971
|
}
|
|
4966
4972
|
|
|
4967
4973
|
/**
|
|
4968
|
-
* Значення `data
|
|
4969
|
-
* Повертає значення першої `add`/`replace`-операції на `/data
|
|
4974
|
+
* Значення `data.<dataKey>` з JSON6902-патчу (масив операцій).
|
|
4975
|
+
* Повертає значення першої `add`/`replace`-операції на `/data/<dataKey>`.
|
|
4970
4976
|
* @param {unknown[]} ops масив JSON6902-операцій
|
|
4977
|
+
* @param {string} dataPointer JSON-Pointer ключа (`/data/<dataKey>`)
|
|
4971
4978
|
* @returns {string | null} присвоєне значення (рядок) або null
|
|
4972
4979
|
*/
|
|
4973
|
-
function
|
|
4980
|
+
function hasuraDataKeyValueFromJson6902(ops, dataPointer) {
|
|
4974
4981
|
for (const item of ops) {
|
|
4975
4982
|
if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
|
|
4976
4983
|
const rec = /** @type {Record<string, unknown>} */ (item)
|
|
4977
4984
|
const op = typeof rec.op === 'string' ? rec.op.trim().toLowerCase() : ''
|
|
4978
4985
|
const path = typeof rec.path === 'string' ? normalizeJsonPatchPath(rec.path) : ''
|
|
4979
|
-
if ((op === 'add' || op === 'replace') && path ===
|
|
4986
|
+
if ((op === 'add' || op === 'replace') && path === dataPointer) {
|
|
4980
4987
|
return typeof rec.value === 'string' ? rec.value : JSON.stringify(rec.value)
|
|
4981
4988
|
}
|
|
4982
4989
|
}
|
|
@@ -4984,42 +4991,58 @@ function enabledApisValueFromJson6902(ops) {
|
|
|
4984
4991
|
}
|
|
4985
4992
|
|
|
4986
4993
|
/**
|
|
4987
|
-
* Значення `data
|
|
4994
|
+
* Значення `data.<dataKey>` зі Strategic-Merge-патчу (об'єкт із `data`).
|
|
4988
4995
|
* @param {object} parsed розпарсений об'єкт patch
|
|
4996
|
+
* @param {string} dataKey ключ у `data` (наприклад `HASURA_GRAPHQL_ENABLED_APIS`)
|
|
4989
4997
|
* @returns {string | null} присвоєне значення (рядок) або null
|
|
4990
4998
|
*/
|
|
4991
|
-
function
|
|
4999
|
+
function hasuraDataKeyValueFromStrategicMerge(parsed, dataKey) {
|
|
4992
5000
|
const data = /** @type {Record<string, unknown>} */ (parsed).data
|
|
4993
5001
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) return null
|
|
4994
5002
|
const d = /** @type {Record<string, unknown>} */ (data)
|
|
4995
|
-
if (!Object.hasOwn(d,
|
|
4996
|
-
const v = d
|
|
5003
|
+
if (!Object.hasOwn(d, dataKey)) return null
|
|
5004
|
+
const v = d[dataKey]
|
|
4997
5005
|
return typeof v === 'string' ? v : JSON.stringify(v)
|
|
4998
5006
|
}
|
|
4999
5007
|
|
|
5000
5008
|
/**
|
|
5001
|
-
* Значення, яке inline-`patch` присвоює `data
|
|
5002
|
-
* **JSON6902** (`op` add/replace на `/data
|
|
5003
|
-
*
|
|
5009
|
+
* Значення, яке inline-`patch` присвоює `data.<dataKey>`. Підтримка двох форматів:
|
|
5010
|
+
* **JSON6902** (`op` add/replace на `/data/<dataKey>`) і **Strategic Merge** (`data.<dataKey>`).
|
|
5011
|
+
* Зовнішні patch-файли (`patches[].path`) не охоплені — Plan B trade-off.
|
|
5004
5012
|
* @param {string} patchText вміст поля `patch`
|
|
5013
|
+
* @param {string} dataKey ключ у `data`
|
|
5014
|
+
* @param {string} dataPointer JSON-Pointer ключа (`/data/<dataKey>`)
|
|
5005
5015
|
* @returns {string | null} присвоєне значення (рядок) або null, якщо patch не чіпає цей ключ
|
|
5006
5016
|
*/
|
|
5007
|
-
|
|
5017
|
+
function hasuraDataKeyValueFromPatchText(patchText, dataKey, dataPointer) {
|
|
5008
5018
|
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
5009
5019
|
if (t === '') return null
|
|
5010
5020
|
const parsed = firstValidYamlJsonFromPatchText(t)
|
|
5011
|
-
if (Array.isArray(parsed)) return
|
|
5021
|
+
if (Array.isArray(parsed)) return hasuraDataKeyValueFromJson6902(parsed, dataPointer)
|
|
5012
5022
|
if (parsed === null || typeof parsed !== 'object') return null
|
|
5013
|
-
return
|
|
5023
|
+
return hasuraDataKeyValueFromStrategicMerge(parsed, dataKey)
|
|
5014
5024
|
}
|
|
5015
5025
|
|
|
5016
5026
|
/**
|
|
5017
|
-
* Значення, яке `
|
|
5027
|
+
* Значення, яке inline-`patch` присвоює `data.HASURA_GRAPHQL_ENABLED_APIS`. Підтримка двох форматів:
|
|
5028
|
+
* **JSON6902** (`op` add/replace на `/data/HASURA_GRAPHQL_ENABLED_APIS`) і **Strategic Merge**
|
|
5029
|
+
* (`data.HASURA_GRAPHQL_ENABLED_APIS`). Зовнішні patch-файли (`patches[].path`) не охоплені — Plan B trade-off.
|
|
5030
|
+
* @param {string} patchText вміст поля `patch`
|
|
5031
|
+
* @returns {string | null} присвоєне значення (рядок) або null, якщо patch не чіпає цей ключ
|
|
5032
|
+
*/
|
|
5033
|
+
export function enabledApisValueFromPatchText(patchText) {
|
|
5034
|
+
return hasuraDataKeyValueFromPatchText(patchText, 'HASURA_GRAPHQL_ENABLED_APIS', HASURA_ENABLED_APIS_DATA_POINTER)
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
/**
|
|
5038
|
+
* Значення, яке `patches[]` kustomization присвоюють `data.<dataKey>` на цілі **ConfigMap**.
|
|
5018
5039
|
* Повертає значення першого patch-а, що чіпає цей ключ, або null, якщо такого немає.
|
|
5019
5040
|
* @param {Record<string, unknown>} kust об'єкт kustomization.yaml
|
|
5041
|
+
* @param {string} dataKey ключ у `data`
|
|
5042
|
+
* @param {string} dataPointer JSON-Pointer ключа (`/data/<dataKey>`)
|
|
5020
5043
|
* @returns {string | null} присвоєне значення або null
|
|
5021
5044
|
*/
|
|
5022
|
-
|
|
5045
|
+
function hasuraDataKeyOverrideValue(kust, dataKey, dataPointer) {
|
|
5023
5046
|
const patches = kust.patches
|
|
5024
5047
|
if (!Array.isArray(patches)) return null
|
|
5025
5048
|
for (const p of patches) {
|
|
@@ -5027,12 +5050,32 @@ export function hasuraEnabledApisOverrideValue(kust) {
|
|
|
5027
5050
|
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
5028
5051
|
if (typeof pr.patch !== 'string') continue
|
|
5029
5052
|
if (resolvePatchTargetKind(pr) !== 'ConfigMap') continue
|
|
5030
|
-
const v =
|
|
5053
|
+
const v = hasuraDataKeyValueFromPatchText(pr.patch, dataKey, dataPointer)
|
|
5031
5054
|
if (v !== null) return v
|
|
5032
5055
|
}
|
|
5033
5056
|
return null
|
|
5034
5057
|
}
|
|
5035
5058
|
|
|
5059
|
+
/**
|
|
5060
|
+
* Значення, яке `patches[]` kustomization присвоюють `data.HASURA_GRAPHQL_ENABLED_APIS` на цілі **ConfigMap**.
|
|
5061
|
+
* Повертає значення першого patch-а, що чіпає цей ключ, або null, якщо такого немає.
|
|
5062
|
+
* @param {Record<string, unknown>} kust об'єкт kustomization.yaml
|
|
5063
|
+
* @returns {string | null} присвоєне значення або null
|
|
5064
|
+
*/
|
|
5065
|
+
export function hasuraEnabledApisOverrideValue(kust) {
|
|
5066
|
+
return hasuraDataKeyOverrideValue(kust, 'HASURA_GRAPHQL_ENABLED_APIS', HASURA_ENABLED_APIS_DATA_POINTER)
|
|
5067
|
+
}
|
|
5068
|
+
|
|
5069
|
+
/**
|
|
5070
|
+
* Значення, яке `patches[]` kustomization присвоюють `data.HASURA_GRAPHQL_ENABLED_LOG_TYPES` на цілі **ConfigMap**.
|
|
5071
|
+
* Повертає значення першого patch-а, що чіпає цей ключ, або null, якщо такого немає.
|
|
5072
|
+
* @param {Record<string, unknown>} kust об'єкт kustomization.yaml
|
|
5073
|
+
* @returns {string | null} присвоєне значення або null
|
|
5074
|
+
*/
|
|
5075
|
+
export function hasuraEnabledLogTypesOverrideValue(kust) {
|
|
5076
|
+
return hasuraDataKeyOverrideValue(kust, 'HASURA_GRAPHQL_ENABLED_LOG_TYPES', HASURA_ENABLED_LOG_TYPES_DATA_POINTER)
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5036
5079
|
/**
|
|
5037
5080
|
* Для кожного **non-base/dev** overlay `kustomization.yaml`, що успадковує Hasura-base (Deployment з
|
|
5038
5081
|
* `hasura/graphql-engine`), вимагає у `patches[]` перевизначення **`data.HASURA_GRAPHQL_ENABLED_APIS`**
|
|
@@ -5070,6 +5113,43 @@ async function validateHasuraOverlayEnabledApisOverride(root, yamlFilesAbs, fail
|
|
|
5070
5113
|
}
|
|
5071
5114
|
}
|
|
5072
5115
|
|
|
5116
|
+
/**
|
|
5117
|
+
* Для кожного **non-base/dev** overlay `kustomization.yaml`, що успадковує Hasura-base (Deployment з
|
|
5118
|
+
* `hasura/graphql-engine`), вимагає у `patches[]` перевизначення **`data.HASURA_GRAPHQL_ENABLED_LOG_TYPES`**
|
|
5119
|
+
* до **`"startup"`** (`http-log` лишається строго для base/dev — прод не пише per-request access-лог у
|
|
5120
|
+
* Cloud Logging). `kind: Component` пропускається (env-неутральне джерело, не overlay).
|
|
5121
|
+
* @param {string} root корінь репозиторію
|
|
5122
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
5123
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
5124
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5125
|
+
*/
|
|
5126
|
+
async function validateHasuraOverlayEnabledLogTypesOverride(root, yamlFilesAbs, fail, passFn) {
|
|
5127
|
+
const rootNorm = resolve(root)
|
|
5128
|
+
const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
|
|
5129
|
+
for (const kustAbs of kustFiles) {
|
|
5130
|
+
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
5131
|
+
const segment = k8sEnvSegmentFromRelPath(rel)
|
|
5132
|
+
if (segment === null || segment === 'base' || segment === 'dev') continue
|
|
5133
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
5134
|
+
if (kust === null || kust.kind === 'Component') continue
|
|
5135
|
+
if (!(await kustomizationTreeHasHasuraDeployment(kustAbs, rootNorm))) continue
|
|
5136
|
+
const value = hasuraEnabledLogTypesOverrideValue(kust)
|
|
5137
|
+
if (value === HASURA_OVERLAY_ENABLED_LOG_TYPES) {
|
|
5138
|
+
passFn(
|
|
5139
|
+
`${rel}: overlay '${segment}' перевизначає HASURA_GRAPHQL_ENABLED_LOG_TYPES="${HASURA_OVERLAY_ENABLED_LOG_TYPES}" (k8s.mdc)`
|
|
5140
|
+
)
|
|
5141
|
+
} else if (value === null) {
|
|
5142
|
+
fail(
|
|
5143
|
+
`${rel}: overlay '${segment}' має у patches[] перевизначати data.HASURA_GRAPHQL_ENABLED_LOG_TYPES до "${HASURA_OVERLAY_ENABLED_LOG_TYPES}" (http-log лише для base/dev) (k8s.mdc)`
|
|
5144
|
+
)
|
|
5145
|
+
} else {
|
|
5146
|
+
fail(
|
|
5147
|
+
`${rel}: overlay '${segment}' patch data.HASURA_GRAPHQL_ENABLED_LOG_TYPES має бути "${HASURA_OVERLAY_ENABLED_LOG_TYPES}" (зараз: ${JSON.stringify(value)}) (k8s.mdc)`
|
|
5148
|
+
)
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5073
5153
|
/**
|
|
5074
5154
|
* Шукає HPA за `scaleTargetRef.name` серед документів.
|
|
5075
5155
|
* @param {Record<string, unknown>[]} hpaDocs масив HPA-документів
|
|
@@ -6658,6 +6738,8 @@ export async function lint(ctx) {
|
|
|
6658
6738
|
|
|
6659
6739
|
await validateHasuraOverlayEnabledApisOverride(root, yamlFiles, fail, pass)
|
|
6660
6740
|
|
|
6741
|
+
await validateHasuraOverlayEnabledLogTypesOverride(root, yamlFiles, fail, pass)
|
|
6742
|
+
|
|
6661
6743
|
return reporter.result()
|
|
6662
6744
|
}
|
|
6663
6745
|
|
|
@@ -3,7 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/text/run-v8r/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 5c8aab7b
|
|
7
|
+
model: manual
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
## Огляд
|
|
@@ -20,18 +21,20 @@ docgen:
|
|
|
20
21
|
- `V8R_CACHE_TTL_SECONDS` — TTL HTTP-кешу v8r (доба замість дефолтних 600 с): fallback-фетчі schemastore не повторюються на кожен прогін.
|
|
21
22
|
- `resolveCustomCatalogSchemas()` — читає джерельний каталог, повертає масив схем у форматі v8r `customCatalog.schemas` (ключ `location`, локальні шляхи — абсолютні).
|
|
22
23
|
- `writeResolvedV8rConfig()` — записує `{ cacheTtl, customCatalog: { schemas } }` у `RESOLVED_V8R_CONFIG_PATH` (побічний ефект: файлова операція запису, поза rollback-механізмом лінт-пайплайна).
|
|
23
|
-
- `runV8rWithGlobs(globs?)` — послідовно запускає `v8r` для кожного glob-у з `V8R_CONFIG_FILE`, вказаним на резольвнутий конфіг; при
|
|
24
|
-
- `runV8rWithFiles(files)` — delta-режим: один запуск `v8r` по конкретних існуючих шляхах (код 98 неможливий), порожній список — одразу 0
|
|
24
|
+
- `runV8rWithGlobs(globs?)` — послідовно запускає `v8r` для кожного glob-у з `V8R_CONFIG_FILE`, вказаним на резольвнутий конфіг; повертає `{ code, detail }` (не голий код): при `code` 0/98 вивід приховано, `detail` порожній; інакше `detail` друкується й повертається код першого невдалого прогону.
|
|
25
|
+
- `runV8rWithFiles(files)` — delta-режим: один запуск `v8r` по конкретних існуючих шляхах (код 98 неможливий), порожній список — одразу `{ code: 0, detail: '' }`.
|
|
26
|
+
- `extractFailureLines(combinedText)` — фільтрує `ℹ`-шум і bunx install-вивід з об'єднаного `stdout+stderr` одного запуску `v8r`, лишаючи предметну деталь (`✖ …`-заголовки, ajv-причини). Обидва потоки об'єднуються навмисно: `v8r` непослідовно розкидає деталь між ними залежно від типу помилки (schema violation — причина в stdout, заголовок у stderr; "не знайдено схему" — усе в stderr).
|
|
25
27
|
- `warnAboutRemoteSchemaFallback(stderrText)` — парсить stderr `v8r` і на кожен файл, чию схему знайдено мережевим fallback-ом (schemastore, не customCatalog), друкує в stdout пораду додати схему в каталог.
|
|
26
28
|
- `stripBunNodeShimDirs(pathValue)` — прибирає з PATH shim-теки `bun-node-*`, які `bun run --bun` додає з підміненим `node`: дочірній `v8r` (node-shebang) інакше виконувався б під bun і падав на непідтримуваному `node:sea`. Дочірній процес `v8r` завжди отримує очищений PATH.
|
|
27
|
-
- `lint(ctx)` — detector `text/run-v8r`: `ctx.files` → delta по відфільтрованих розширеннях, без `ctx.files` → full за `DEFAULT_V8R_GLOBS`.
|
|
29
|
+
- `lint(ctx)` — detector `text/run-v8r`: `ctx.files` → delta по відфільтрованих розширеннях, без `ctx.files` → full за `DEFAULT_V8R_GLOBS`. Порушення несе `detail` (якщо є) у тексті `fail()`-повідомлення — без нього LLM fix-worker бачив лише "щось не пройшло" й незмінно провалював усі rung-и драбини (не мав інформації, що саме виправляти).
|
|
28
30
|
|
|
29
31
|
## Публічний API
|
|
30
32
|
|
|
31
33
|
- `DEFAULT_V8R_GLOBS`, `V8R_CATALOG_PATH`, `RESOLVED_V8R_CONFIG_PATH`, `V8R_CACHE_TTL_SECONDS` — константи.
|
|
32
34
|
- `resolveCustomCatalogSchemas()` — обчислені схеми customCatalog.
|
|
33
35
|
- `writeResolvedV8rConfig()` — матеріалізація тимчасового конфігу, повертає його шлях.
|
|
34
|
-
- `runV8rWithGlobs(globs?)`, `runV8rWithFiles(files)` — точки входу перевірки (full/delta)
|
|
36
|
+
- `runV8rWithGlobs(globs?)`, `runV8rWithFiles(files)` — точки входу перевірки (full/delta), повертають `{ code, detail }`.
|
|
37
|
+
- `extractFailureLines(combinedText)` — фільтр noise-рядків для деталі провалу.
|
|
35
38
|
- `warnAboutRemoteSchemaFallback(stderrText)` — попередження про мережевий fallback схем.
|
|
36
39
|
- `stripBunNodeShimDirs(pathValue)` — PATH без shim-тек `bun-node-*` (для дочірнього v8r).
|
|
37
40
|
- `lint(ctx)` — інтеграція в lint-пайплайн.
|
|
@@ -114,17 +114,28 @@ export function writeResolvedV8rConfig() {
|
|
|
114
114
|
|
|
115
115
|
const PROCESSING_LINE_RE = /^ℹ Processing (.+)$/u
|
|
116
116
|
const FOUND_REMOTE_SCHEMA_RE = /^ℹ Found schema in (https?:\/\/\S+)/u
|
|
117
|
-
const
|
|
117
|
+
const NOISE_LINE_RE = /^(?:ℹ .*|Resolving dependencies|Resolved, downloaded and extracted.*|Saved lockfile)$/u
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
120
|
+
* Прибирає v8r/bunx noise-рядки (весь `ℹ`-статус: Loaded config file, Patterns and relative
|
|
121
|
+
* paths, Pre-warming the cache, Processing <file>, Found schema in …, Validating …; і службовий
|
|
122
|
+
* вивід bunx-встановлення Resolving/Resolved/Saved lockfile) з об'єднаного stdout+stderr одного
|
|
123
|
+
* запуску —
|
|
124
|
+
* лишає предметну деталь (`✖ …`-заголовки й ajv-причини на кшталт "must NOT have additional
|
|
125
|
+
* properties…"). Обидва потоки об'єднуються НАВМИСНО: v8r непослідовно розкидає ці рядки між
|
|
126
|
+
* stdout/stderr залежно від типу помилки — при порушенні схеми ajv-причина йде у stdout, а
|
|
127
|
+
* `✖ … is invalid`-заголовок у stderr; при "не знайдено схему" все йде у stderr, stdout
|
|
128
|
+
* порожній. Фільтр лише за stdout (як раніше) на другому випадку повертав би зовсім порожню
|
|
129
|
+
* деталь — і LLM fix-worker (як і non-verbose CLI-підсумок) не бачив би жодної причини провалу.
|
|
130
|
+
* @param {string} combinedText stdout + '\n' + stderr одного запуску v8r
|
|
131
|
+
* @returns {string} відфільтровані непорожні рядки, join('\n') (порожній рядок, якщо деталі нема)
|
|
125
132
|
*/
|
|
126
|
-
export function extractFailureLines(
|
|
127
|
-
return
|
|
133
|
+
export function extractFailureLines(combinedText) {
|
|
134
|
+
return combinedText
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map(line => line.trim())
|
|
137
|
+
.filter(line => line.length > 0 && !NOISE_LINE_RE.test(line))
|
|
138
|
+
.join('\n')
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
/**
|
|
@@ -168,11 +179,16 @@ export function stripBunNodeShimDirs(pathValue) {
|
|
|
168
179
|
|
|
169
180
|
/**
|
|
170
181
|
* Один виклик `bun x v8r <targets...>` з підготовленим `customCatalog`-конфігом.
|
|
182
|
+
* `detail` (рядки `✖ …`) обчислюється завжди, незалежно від `verbose` — потрібен викликачу
|
|
183
|
+
* (`lint()`) для вбудовування у violation-повідомлення, яке бачить LLM fix-worker: без нього
|
|
184
|
+
* fix-ladder отримує лише "щось не пройшло" й не має шансів вгадати, що саме (спостережено —
|
|
185
|
+
* усі 4 rung-и незмінно падають у timeout на v8r-порушеннях).
|
|
171
186
|
* @param {string[]} targets glob-и або конкретні шляхи файлів
|
|
172
187
|
* @param {string} configPath шлях до `V8R_CONFIG_FILE`
|
|
173
188
|
* @param {boolean} [verbose] друкувати повний raw stdout/stderr v8r при помилці; інакше — лише
|
|
174
189
|
* рядки `✖ …` без `ℹ`-шуму (Pre-warming the cache, Processing <file>, Found schema in …)
|
|
175
|
-
* @returns {{ exitError: true } | { exitError: false, code: number }} помилка
|
|
190
|
+
* @returns {{ exitError: true } | { exitError: false, code: number, detail: string }} помилка
|
|
191
|
+
* spawn або код v8r (0/98 — трактує викликач) + деталь `✖ …`-рядків
|
|
176
192
|
*/
|
|
177
193
|
function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
178
194
|
const bunPath = resolveCmd('bun') ?? process.execPath
|
|
@@ -192,16 +208,17 @@ function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
|
192
208
|
warnAboutRemoteSchemaFallback(result.stderr ?? '')
|
|
193
209
|
|
|
194
210
|
const exitCode = result.status ?? 1
|
|
211
|
+
let detail = ''
|
|
195
212
|
if (exitCode !== 0 && exitCode !== 98) {
|
|
213
|
+
detail = extractFailureLines(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)
|
|
196
214
|
if (verbose) {
|
|
197
215
|
if (result.stdout?.length) process.stdout.write(result.stdout)
|
|
198
216
|
if (result.stderr?.length) process.stderr.write(result.stderr)
|
|
199
|
-
} else {
|
|
200
|
-
|
|
201
|
-
if (failureLines.length) process.stdout.write(`${failureLines}\n`)
|
|
217
|
+
} else if (detail.length) {
|
|
218
|
+
process.stdout.write(`${detail}\n`)
|
|
202
219
|
}
|
|
203
220
|
}
|
|
204
|
-
return { exitError: false, code: exitCode }
|
|
221
|
+
return { exitError: false, code: exitCode, detail }
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
/**
|
|
@@ -210,24 +227,25 @@ function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
|
210
227
|
* glob не знаходить файлів, і тоді решта розширень не перевіряються в тому ж виклику.
|
|
211
228
|
* @param {string[]} [globs] патерни; за замовчуванням DEFAULT_V8R_GLOBS
|
|
212
229
|
* @param {boolean} [verbose] друкувати повний raw вивід v8r при помилці (див. runOneV8rInvocation)
|
|
213
|
-
* @returns {number} 0 — OK, 1 — помилка spawn, 2 — немає
|
|
230
|
+
* @returns {{ code: number, detail: string }} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
|
|
231
|
+
* каталогу схем, інше — код v8r; `detail` — рядки `✖ …` (порожньо, якщо `code` не про валідацію)
|
|
214
232
|
*/
|
|
215
233
|
export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
|
|
216
234
|
if (!existsSync(V8R_CATALOG_PATH)) {
|
|
217
235
|
process.stderr.write(
|
|
218
236
|
`run-v8r: не знайдено каталог схем за шляхом ${V8R_CATALOG_PATH} (очікується npm/schemas/v8r-catalog.json у пакеті)\n`
|
|
219
237
|
)
|
|
220
|
-
return 2
|
|
238
|
+
return { code: 2, detail: '' }
|
|
221
239
|
}
|
|
222
240
|
|
|
223
241
|
const configPath = writeResolvedV8rConfig()
|
|
224
242
|
|
|
225
243
|
for (const pattern of globs) {
|
|
226
244
|
const r = runOneV8rInvocation([pattern], configPath, verbose)
|
|
227
|
-
if (r.exitError) return 1
|
|
228
|
-
if (r.code !== 0 && r.code !== 98) return r.code
|
|
245
|
+
if (r.exitError) return { code: 1, detail: '' }
|
|
246
|
+
if (r.code !== 0 && r.code !== 98) return { code: r.code, detail: r.detail }
|
|
229
247
|
}
|
|
230
|
-
return 0
|
|
248
|
+
return { code: 0, detail: '' }
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
/**
|
|
@@ -235,21 +253,33 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
|
|
|
235
253
|
* бо кожен переданий шлях уже існує (не glob), тож код 98 "порожній glob" тут не виникає.
|
|
236
254
|
* @param {string[]} files абсолютні або відносні до cwd v8r-процесу шляхи файлів
|
|
237
255
|
* @param {boolean} [verbose] друкувати повний raw вивід v8r при помилці (див. runOneV8rInvocation)
|
|
238
|
-
* @returns {number} 0 — OK, 1 — помилка spawn, 2 — немає
|
|
256
|
+
* @returns {{ code: number, detail: string }} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
|
|
257
|
+
* каталогу схем, інше — код v8r; `detail` — рядки `✖ …` (порожньо, якщо `code` не про валідацію)
|
|
239
258
|
*/
|
|
240
259
|
export function runV8rWithFiles(files, verbose = false) {
|
|
241
|
-
if (files.length === 0) return 0
|
|
260
|
+
if (files.length === 0) return { code: 0, detail: '' }
|
|
242
261
|
if (!existsSync(V8R_CATALOG_PATH)) {
|
|
243
262
|
process.stderr.write(
|
|
244
263
|
`run-v8r: не знайдено каталог схем за шляхом ${V8R_CATALOG_PATH} (очікується npm/schemas/v8r-catalog.json у пакеті)\n`
|
|
245
264
|
)
|
|
246
|
-
return 2
|
|
265
|
+
return { code: 2, detail: '' }
|
|
247
266
|
}
|
|
248
267
|
|
|
249
268
|
const configPath = writeResolvedV8rConfig()
|
|
250
269
|
const r = runOneV8rInvocation(files, configPath, verbose)
|
|
251
|
-
if (r.exitError) return 1
|
|
252
|
-
return r.code === 98 ? 0 : r.code
|
|
270
|
+
if (r.exitError) return { code: 1, detail: '' }
|
|
271
|
+
return { code: r.code === 98 ? 0 : r.code, detail: r.detail }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Будує violation-повідомлення з опційною деталлю `✖ …`-рядків v8r — без неї LLM fix-worker
|
|
276
|
+
* бачить лише "щось не пройшло" й не має інформації, який файл/поле саме порушує схему.
|
|
277
|
+
* @param {string} detail рядки `✖ …` з `runV8rWithGlobs`/`runV8rWithFiles` (може бути порожнім)
|
|
278
|
+
* @returns {string} повне повідомлення для `fail()`
|
|
279
|
+
*/
|
|
280
|
+
function v8rFailMessage(detail) {
|
|
281
|
+
const base = 'v8r schema-валідація json/yaml/toml не пройшла (text.mdc)'
|
|
282
|
+
return detail ? `${base}:\n${detail}` : base
|
|
253
283
|
}
|
|
254
284
|
|
|
255
285
|
/**
|
|
@@ -264,19 +294,19 @@ export function lint(ctx) {
|
|
|
264
294
|
const verbose = ctx.verbose === true
|
|
265
295
|
|
|
266
296
|
if (ctx.files === undefined) {
|
|
267
|
-
const code = runV8rWithGlobs(DEFAULT_V8R_GLOBS, verbose)
|
|
268
|
-
if (code !== 0) fail(
|
|
297
|
+
const { code, detail } = runV8rWithGlobs(DEFAULT_V8R_GLOBS, verbose)
|
|
298
|
+
if (code !== 0) fail(v8rFailMessage(detail), 'v8r')
|
|
269
299
|
return reporter.result()
|
|
270
300
|
}
|
|
271
301
|
|
|
272
302
|
const files = ctx.files.filter(f => V8R_EXT_RE.test(f))
|
|
273
303
|
if (files.length === 0) return reporter.result()
|
|
274
|
-
const code = runV8rWithFiles(files, verbose)
|
|
275
|
-
if (code !== 0) fail(
|
|
304
|
+
const { code, detail } = runV8rWithFiles(files, verbose)
|
|
305
|
+
if (code !== 0) fail(v8rFailMessage(detail), 'v8r')
|
|
276
306
|
return reporter.result()
|
|
277
307
|
}
|
|
278
308
|
|
|
279
309
|
if (isRunAsCli(import.meta.url)) {
|
|
280
310
|
const globs = process.argv.length > 2 ? process.argv.slice(2) : DEFAULT_V8R_GLOBS
|
|
281
|
-
process.exitCode = runV8rWithGlobs(globs)
|
|
311
|
+
process.exitCode = runV8rWithGlobs(globs).code
|
|
282
312
|
}
|