@nitra/cursor 5.3.0 → 5.3.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 +6 -0
- package/bin/n-cursor.js +72 -50
- package/lib/models.mjs +1 -1
- package/package.json +1 -1
- package/rules/k8s/js/manifests.mjs +144 -82
- package/rules/npm-module/js/header_doc_pointer.mjs +72 -27
- package/rules/npm-module/js/rule_meta.mjs +72 -36
- package/rules/npm-module/js/skill_meta.mjs +59 -35
- package/scripts/coverage-classify/index.mjs +2 -2
- package/scripts/coverage-classify/verdict-schema.mjs +1 -1
- package/scripts/lib/assert-project-root.mjs +1 -1
- package/scripts/lib/discover-check-rules-from-cursor.mjs +1 -1
- package/scripts/lib/rule-predicates.mjs +30 -18
- package/scripts/lib/run-rule-cli.mjs +1 -1
- package/scripts/lib/run-standard-rule.mjs +1 -1
- package/scripts/post-tool-use-fix.mjs +3 -3
- package/scripts/skills-cli.mjs +5 -5
- package/scripts/worktree-cli.mjs +5 -5
- package/skills/doc-files/js/docgen-extract.mjs +1 -1
- package/skills/doc-files/js/docgen-files-batch.mjs +65 -34
- package/skills/doc-files/js/docgen-gen.mjs +121 -36
- package/skills/doc-files/js/docgen-prompts.mjs +20 -5
- package/skills/fix/js/orchestrator.mjs +64 -35
- package/skills/fix/js/t0.mjs +44 -32
- package/skills/start-check/js/check.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.3.1] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- lint: усунено eslint/oxlint-помилки (24) — ігнор кореневого `.worktrees/` в eslint.config, GENERIC_RE→масив дрібних patternів (sonarjs/regex-complexity), Array#sort→toSorted; cspell: додано `ollama` у словник
|
|
8
|
+
|
|
3
9
|
## [5.3.0] - 2026-06-11
|
|
4
10
|
|
|
5
11
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -1243,7 +1243,73 @@ function logRemovedManagedItems(title, basePath, names) {
|
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
1245
1245
|
/**
|
|
1246
|
-
*
|
|
1246
|
+
* Визначає список правил для fix: явно задані (з валідацією проти available) або
|
|
1247
|
+
* discovery з `.cursor/rules/*.mdc`. Друкує діагностику й кидає на невідомих правилах.
|
|
1248
|
+
* @param {string[]} requestedRules запитані правила (порожній → discovery)
|
|
1249
|
+
* @param {string[]} available доступні в пакеті rule-id
|
|
1250
|
+
* @param {boolean} json json-режим (впливає на early-return вивід при порожньому discovery)
|
|
1251
|
+
* @returns {Promise<string[]|null>} список id або null — нічого запускати (вивід уже зроблено)
|
|
1252
|
+
*/
|
|
1253
|
+
async function resolveFixRuleIds(requestedRules, available, json) {
|
|
1254
|
+
if (requestedRules.length > 0) {
|
|
1255
|
+
const unknown = requestedRules.filter(id => !available.includes(id))
|
|
1256
|
+
if (unknown.length > 0) {
|
|
1257
|
+
console.error(`❌ Невідомі правила: ${unknown.join(', ')}`)
|
|
1258
|
+
console.log(` Доступні: ${available.join(', ')}`)
|
|
1259
|
+
throw new Error(`Unknown rules: ${unknown.join(', ')}`)
|
|
1260
|
+
}
|
|
1261
|
+
return requestedRules
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const mdcFiles = await listProjectRulesMdcFiles()
|
|
1265
|
+
if (mdcFiles.length === 0) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`Немає файлів *.mdc у ${RULES_DIR}/. Запустіть \`npx ${PACKAGE_NAME}\` або вкажіть правила: \`npx ${PACKAGE_NAME} fix bun ga\``
|
|
1268
|
+
)
|
|
1269
|
+
}
|
|
1270
|
+
const idsToRun = discoverCheckRulesFromCursorRules(available, mdcFiles)
|
|
1271
|
+
if (idsToRun.length === 0) {
|
|
1272
|
+
if (json) {
|
|
1273
|
+
process.stdout.write(`${JSON.stringify({ total: 0, failed: 0, rules: [] })}\n`)
|
|
1274
|
+
return null
|
|
1275
|
+
}
|
|
1276
|
+
console.log(
|
|
1277
|
+
`\n🔍 ${PACKAGE_NAME} fix — у ${RULES_DIR}/ немає правил з programmatic перевіркою ` +
|
|
1278
|
+
`(відповідного fix.mjs у пакеті). Нічого не запущено.\n`
|
|
1279
|
+
)
|
|
1280
|
+
return null
|
|
1281
|
+
}
|
|
1282
|
+
return idsToRun
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Прогоняє `fix.mjs` кожного правила окремим процесом; збирає timings і (для json) per-rule output.
|
|
1287
|
+
* @param {string[]} idsToRun правила до запуску
|
|
1288
|
+
* @param {boolean} json json-режим — захоплює stdout/stderr у структуру замість inherit у термінал
|
|
1289
|
+
* @returns {{ totalFailed: number, timings: {id:string, ms:number, ok:boolean}[], ruleResults: {ruleId:string, ok:boolean, output:string}[] }} підсумок прогону
|
|
1290
|
+
*/
|
|
1291
|
+
function runRuleFixProcesses(idsToRun, json) {
|
|
1292
|
+
let totalFailed = 0
|
|
1293
|
+
const timings = []
|
|
1294
|
+
const ruleResults = []
|
|
1295
|
+
for (const id of idsToRun) {
|
|
1296
|
+
const fixPath = join(BUNDLED_RULES_DIR, id, 'fix.mjs')
|
|
1297
|
+
const startedAt = Date.now()
|
|
1298
|
+
const result = json
|
|
1299
|
+
? spawnSync('bun', [fixPath], { encoding: 'utf8' })
|
|
1300
|
+
: spawnSync('bun', [fixPath], { stdio: 'inherit' })
|
|
1301
|
+
const ok = result.status === 0
|
|
1302
|
+
timings.push({ id: `fix-${id}`, ms: Date.now() - startedAt, ok })
|
|
1303
|
+
if (json) {
|
|
1304
|
+
ruleResults.push({ ruleId: id, ok, output: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim() })
|
|
1305
|
+
}
|
|
1306
|
+
if (!ok) totalFailed++
|
|
1307
|
+
}
|
|
1308
|
+
return { totalFailed, timings, ruleResults }
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Spawn-wrapper для `npx \@nitra/cursor fix [<rule>...]`. Один шлях у коді: для кожного правила
|
|
1247
1313
|
* робить `bun rules/<id>/fix.mjs` як окремий процес. Сам `fix.mjs` читає `.n-cursor.json`,
|
|
1248
1314
|
* перевіряє whitelist (`runRuleCli`) і друкує per-rule summary.
|
|
1249
1315
|
*
|
|
@@ -1272,56 +1338,12 @@ async function runFixCommand(requestedRules, opts = {}) {
|
|
|
1272
1338
|
throw new Error('No rules found')
|
|
1273
1339
|
}
|
|
1274
1340
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
console.error(`❌ Невідомі правила: ${unknown.join(', ')}`)
|
|
1280
|
-
console.log(` Доступні: ${available.join(', ')}`)
|
|
1281
|
-
throw new Error(`Unknown rules: ${unknown.join(', ')}`)
|
|
1282
|
-
}
|
|
1283
|
-
idsToRun = requestedRules
|
|
1284
|
-
} else {
|
|
1285
|
-
const mdcFiles = await listProjectRulesMdcFiles()
|
|
1286
|
-
if (mdcFiles.length === 0) {
|
|
1287
|
-
throw new Error(
|
|
1288
|
-
`Немає файлів *.mdc у ${RULES_DIR}/. Запустіть \`npx ${PACKAGE_NAME}\` або вкажіть правила: \`npx ${PACKAGE_NAME} fix bun ga\``
|
|
1289
|
-
)
|
|
1290
|
-
}
|
|
1291
|
-
idsToRun = discoverCheckRulesFromCursorRules(available, mdcFiles)
|
|
1292
|
-
if (idsToRun.length === 0) {
|
|
1293
|
-
if (json) {
|
|
1294
|
-
process.stdout.write(`${JSON.stringify({ total: 0, failed: 0, rules: [] })}\n`)
|
|
1295
|
-
return
|
|
1296
|
-
}
|
|
1297
|
-
console.log(
|
|
1298
|
-
`\n🔍 ${PACKAGE_NAME} fix — у ${RULES_DIR}/ немає правил з programmatic перевіркою ` +
|
|
1299
|
-
`(відповідного fix.mjs у пакеті). Нічого не запущено.\n`
|
|
1300
|
-
)
|
|
1301
|
-
return
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1341
|
+
// json-режим: захоплюємо stdout/stderr правила у структуру (а не inherit у термінал),
|
|
1342
|
+
// щоб віддати агенту згруповано {ruleId, ok, output} і він читав лише впалі.
|
|
1343
|
+
const idsToRun = await resolveFixRuleIds(requestedRules, available, json)
|
|
1344
|
+
if (idsToRun === null) return
|
|
1304
1345
|
|
|
1305
|
-
|
|
1306
|
-
/** @type {{ id: string, ms: number, ok: boolean }[]} */
|
|
1307
|
-
const timings = []
|
|
1308
|
-
/** @type {{ ruleId: string, ok: boolean, output: string }[]} */
|
|
1309
|
-
const ruleResults = []
|
|
1310
|
-
for (const id of idsToRun) {
|
|
1311
|
-
const fixPath = join(BUNDLED_RULES_DIR, id, 'fix.mjs')
|
|
1312
|
-
const startedAt = Date.now()
|
|
1313
|
-
// json-режим: захоплюємо stdout/stderr правила у структуру (а не inherit у термінал),
|
|
1314
|
-
// щоб віддати агенту згруповано {ruleId, ok, output} і він читав лише впалі.
|
|
1315
|
-
const result = json
|
|
1316
|
-
? spawnSync('bun', [fixPath], { encoding: 'utf8' })
|
|
1317
|
-
: spawnSync('bun', [fixPath], { stdio: 'inherit' })
|
|
1318
|
-
const ok = result.status === 0
|
|
1319
|
-
timings.push({ id: `fix-${id}`, ms: Date.now() - startedAt, ok })
|
|
1320
|
-
if (json) {
|
|
1321
|
-
ruleResults.push({ ruleId: id, ok, output: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim() })
|
|
1322
|
-
}
|
|
1323
|
-
if (!ok) totalFailed++
|
|
1324
|
-
}
|
|
1346
|
+
const { totalFailed, timings, ruleResults } = runRuleFixProcesses(idsToRun, json)
|
|
1325
1347
|
|
|
1326
1348
|
if (json) {
|
|
1327
1349
|
process.stdout.write(`${JSON.stringify({ total: idsToRun.length, failed: totalFailed, rules: ruleResults })}\n`)
|
package/lib/models.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## Бекенд за префіксом model-id
|
|
16
16
|
*
|
|
17
|
-
* model-id з префіксом `omlx/...`
|
|
17
|
+
* model-id з префіксом `omlx/...` іде прямим HTTP до локального
|
|
18
18
|
* omlx-сервера (`npm/lib/omlx.mjs`), минаючи pi; решта (`openai/...`,
|
|
19
19
|
* `ollama/...`, '') — через pi CLI. Тому локальні тири варто задавати у форматі
|
|
20
20
|
* `omlx/<model>`, аби local-inference йшов напряму, а pi лишався шаром для хмари
|
package/package.json
CHANGED
|
@@ -3942,7 +3942,7 @@ export function loadSnippetSpec(snippetName) {
|
|
|
3942
3942
|
const url = NETWORK_POLICY_SNIPPET_URLS[snippetName]
|
|
3943
3943
|
if (!url) throw new Error(`Unknown NetworkPolicy snippet: ${snippetName}`)
|
|
3944
3944
|
const raw = readFileSync(fileURLToPath(url), 'utf8')
|
|
3945
|
-
_snippetCache[snippetName] = /** @type {
|
|
3945
|
+
_snippetCache[snippetName] = /** @type {{ spec: Record<string, unknown> }} */ (parseDocument(raw).toJS()).spec
|
|
3946
3946
|
return _snippetCache[snippetName]
|
|
3947
3947
|
}
|
|
3948
3948
|
|
|
@@ -3981,7 +3981,7 @@ const noopFail = msg => msg
|
|
|
3981
3981
|
/**
|
|
3982
3982
|
* `from`-peers для HTTPRoute-aware ingress-правила (GCP HC global + Envoy data-plane / proxy-only subnet).
|
|
3983
3983
|
* Порядок зафіксовано детерміністичним (HC-global → 10.0.0.0/8). Див. розділ «HTTPRoute → NetworkPolicy ingress» у k8s.mdc.
|
|
3984
|
-
* @type {
|
|
3984
|
+
* @type {readonly { ipBlock: { cidr: string } }[]}
|
|
3985
3985
|
*/
|
|
3986
3986
|
const NETWORK_POLICY_GCLB_INGRESS_FROM = Object.freeze([
|
|
3987
3987
|
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
@@ -4061,45 +4061,7 @@ export async function collectHttpRouteIngressForWorkload(dir, appLabel, fail) {
|
|
|
4061
4061
|
const servicesByName = new Map()
|
|
4062
4062
|
|
|
4063
4063
|
for (const abs of yamlFiles) {
|
|
4064
|
-
|
|
4065
|
-
try {
|
|
4066
|
-
raw = await readFile(abs, 'utf8')
|
|
4067
|
-
} catch (error) {
|
|
4068
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
4069
|
-
fail(`${abs}: не вдалося прочитати для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
|
|
4070
|
-
continue
|
|
4071
|
-
}
|
|
4072
|
-
const lines = toLines(raw)
|
|
4073
|
-
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
4074
|
-
/** @type {import('yaml').Document[]} */
|
|
4075
|
-
let docs
|
|
4076
|
-
try {
|
|
4077
|
-
docs = parseAllDocuments(body)
|
|
4078
|
-
} catch (error) {
|
|
4079
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
4080
|
-
fail(`${abs}: не вдалося розібрати YAML для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
|
|
4081
|
-
continue
|
|
4082
|
-
}
|
|
4083
|
-
for (const doc of docs) {
|
|
4084
|
-
if (doc.errors.length > 0) {
|
|
4085
|
-
fail(
|
|
4086
|
-
`${abs}: YAML містить помилки для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${doc.errors[0].message}`
|
|
4087
|
-
)
|
|
4088
|
-
continue
|
|
4089
|
-
}
|
|
4090
|
-
const rec = asPlainRecord(doc.toJSON())
|
|
4091
|
-
if (rec === null) continue
|
|
4092
|
-
const av = rec.apiVersion
|
|
4093
|
-
if (rec.kind === 'HTTPRoute' && typeof av === 'string' && av.startsWith(GATEWAY_API_GROUP_PREFIX)) {
|
|
4094
|
-
collectHttpRouteBackendRefsInto(rec.spec, allBackendRefs)
|
|
4095
|
-
} else if (rec.kind === 'Service') {
|
|
4096
|
-
const name = manifestMetadataName(rec)
|
|
4097
|
-
if (name !== null) {
|
|
4098
|
-
const app = serviceSelectorAppLabel(rec.spec)
|
|
4099
|
-
if (app !== null) servicesByName.set(name, app)
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4102
|
-
}
|
|
4064
|
+
await collectHttpRouteFileInto(abs, allBackendRefs, servicesByName, fail)
|
|
4103
4065
|
}
|
|
4104
4066
|
|
|
4105
4067
|
/** @type {Set<number>} */
|
|
@@ -4112,6 +4074,68 @@ export async function collectHttpRouteIngressForWorkload(dir, appLabel, fail) {
|
|
|
4112
4074
|
return { ports: [...ports].toSorted((a, b) => a - b) }
|
|
4113
4075
|
}
|
|
4114
4076
|
|
|
4077
|
+
/**
|
|
4078
|
+
* Читає й парсить один YAML-файл і акумулює HTTPRoute-backendRefs та Service `app`-мітки.
|
|
4079
|
+
* Read/parse/doc-помилки репортяться через `fail` і не зупиняють обхід (як у оригінальному циклі).
|
|
4080
|
+
* @param {string} abs абсолютний шлях до YAML-файлу
|
|
4081
|
+
* @param {Array<{ name: string, port: number }>} allBackendRefs акумулятор backendRefs
|
|
4082
|
+
* @param {Map<string, string>} servicesByName акумулятор Service name → app-label
|
|
4083
|
+
* @param {(msg: string) => void} fail callback при read/parse-помилці
|
|
4084
|
+
* @returns {Promise<void>} результат
|
|
4085
|
+
*/
|
|
4086
|
+
async function collectHttpRouteFileInto(abs, allBackendRefs, servicesByName, fail) {
|
|
4087
|
+
let raw
|
|
4088
|
+
try {
|
|
4089
|
+
raw = await readFile(abs, 'utf8')
|
|
4090
|
+
} catch (error) {
|
|
4091
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
4092
|
+
fail(`${abs}: не вдалося прочитати для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
|
|
4093
|
+
return
|
|
4094
|
+
}
|
|
4095
|
+
const lines = toLines(raw)
|
|
4096
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
4097
|
+
/** @type {import('yaml').Document[]} */
|
|
4098
|
+
let docs
|
|
4099
|
+
try {
|
|
4100
|
+
docs = parseAllDocuments(body)
|
|
4101
|
+
} catch (error) {
|
|
4102
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
4103
|
+
fail(`${abs}: не вдалося розібрати YAML для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
|
|
4104
|
+
return
|
|
4105
|
+
}
|
|
4106
|
+
for (const doc of docs) {
|
|
4107
|
+
if (doc.errors.length > 0) {
|
|
4108
|
+
fail(
|
|
4109
|
+
`${abs}: YAML містить помилки для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${doc.errors[0].message}`
|
|
4110
|
+
)
|
|
4111
|
+
continue
|
|
4112
|
+
}
|
|
4113
|
+
collectHttpRouteDocInto(doc.toJSON(), allBackendRefs, servicesByName)
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
/**
|
|
4118
|
+
* Класифікує один YAML-документ (HTTPRoute / Service) і акумулює backendRefs / Service `app`-мітку.
|
|
4119
|
+
* @param {unknown} json розпарсений документ
|
|
4120
|
+
* @param {Array<{ name: string, port: number }>} allBackendRefs акумулятор backendRefs
|
|
4121
|
+
* @param {Map<string, string>} servicesByName акумулятор Service name → app-label
|
|
4122
|
+
* @returns {void} результат
|
|
4123
|
+
*/
|
|
4124
|
+
function collectHttpRouteDocInto(json, allBackendRefs, servicesByName) {
|
|
4125
|
+
const rec = asPlainRecord(json)
|
|
4126
|
+
if (rec === null) return
|
|
4127
|
+
const av = rec.apiVersion
|
|
4128
|
+
if (rec.kind === 'HTTPRoute' && typeof av === 'string' && av.startsWith(GATEWAY_API_GROUP_PREFIX)) {
|
|
4129
|
+
collectHttpRouteBackendRefsInto(rec.spec, allBackendRefs)
|
|
4130
|
+
} else if (rec.kind === 'Service') {
|
|
4131
|
+
const name = manifestMetadataName(rec)
|
|
4132
|
+
if (name !== null) {
|
|
4133
|
+
const app = serviceSelectorAppLabel(rec.spec)
|
|
4134
|
+
if (app !== null) servicesByName.set(name, app)
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4115
4139
|
/**
|
|
4116
4140
|
* Витягує мітку `app` з `Service.spec.selector.app` (плоский селектор, без `matchLabels`).
|
|
4117
4141
|
* Окремий helper від `appLabelFromSpecSelector` (той — для Deployment/StatefulSet, де `selector.matchLabels.app`).
|
|
@@ -4824,39 +4848,46 @@ export async function kustomizationTreeHasHasuraDeployment(kustAbs, rootNorm) {
|
|
|
4824
4848
|
}
|
|
4825
4849
|
|
|
4826
4850
|
/**
|
|
4827
|
-
*
|
|
4828
|
-
*
|
|
4829
|
-
*
|
|
4830
|
-
* @param {string} patchText вміст поля `patch`
|
|
4831
|
-
* @returns {string | null} присвоєне значення (рядок) або null, якщо patch не чіпає цей ключ
|
|
4851
|
+
* Парсить `patchText` як YAML і повертає JSON першого документа без помилок (або undefined).
|
|
4852
|
+
* @param {string} patchText непорожній trimmed-текст patch
|
|
4853
|
+
* @returns {unknown} JSON першого валідного документа або undefined
|
|
4832
4854
|
*/
|
|
4833
|
-
|
|
4834
|
-
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
4835
|
-
if (t === '') return null
|
|
4836
|
-
let parsed
|
|
4855
|
+
function firstValidYamlJsonFromPatchText(patchText) {
|
|
4837
4856
|
try {
|
|
4838
|
-
for (const d of parseAllDocuments(
|
|
4839
|
-
if (d.errors.length === 0)
|
|
4840
|
-
parsed = d.toJSON()
|
|
4841
|
-
break
|
|
4842
|
-
}
|
|
4857
|
+
for (const d of parseAllDocuments(patchText)) {
|
|
4858
|
+
if (d.errors.length === 0) return d.toJSON()
|
|
4843
4859
|
}
|
|
4844
4860
|
} catch {
|
|
4845
|
-
return
|
|
4861
|
+
return undefined
|
|
4846
4862
|
}
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4863
|
+
return undefined
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4866
|
+
/**
|
|
4867
|
+
* Значення `data.HASURA_GRAPHQL_ENABLED_APIS` з JSON6902-патчу (масив операцій).
|
|
4868
|
+
* Повертає значення першої `add`/`replace`-операції на `/data/HASURA_GRAPHQL_ENABLED_APIS`.
|
|
4869
|
+
* @param {unknown[]} ops масив JSON6902-операцій
|
|
4870
|
+
* @returns {string | null} присвоєне значення (рядок) або null
|
|
4871
|
+
*/
|
|
4872
|
+
function enabledApisValueFromJson6902(ops) {
|
|
4873
|
+
for (const item of ops) {
|
|
4874
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
|
|
4875
|
+
const rec = /** @type {Record<string, unknown>} */ (item)
|
|
4876
|
+
const op = typeof rec.op === 'string' ? rec.op.trim().toLowerCase() : ''
|
|
4877
|
+
const path = typeof rec.path === 'string' ? normalizeJsonPatchPath(rec.path) : ''
|
|
4878
|
+
if ((op === 'add' || op === 'replace') && path === HASURA_ENABLED_APIS_DATA_POINTER) {
|
|
4879
|
+
return typeof rec.value === 'string' ? rec.value : JSON.stringify(rec.value)
|
|
4856
4880
|
}
|
|
4857
|
-
return null
|
|
4858
4881
|
}
|
|
4859
|
-
|
|
4882
|
+
return null
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
/**
|
|
4886
|
+
* Значення `data.HASURA_GRAPHQL_ENABLED_APIS` зі Strategic-Merge-патчу (об'єкт із `data`).
|
|
4887
|
+
* @param {object} parsed розпарсений об'єкт patch
|
|
4888
|
+
* @returns {string | null} присвоєне значення (рядок) або null
|
|
4889
|
+
*/
|
|
4890
|
+
function enabledApisValueFromStrategicMerge(parsed) {
|
|
4860
4891
|
const data = /** @type {Record<string, unknown>} */ (parsed).data
|
|
4861
4892
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) return null
|
|
4862
4893
|
const d = /** @type {Record<string, unknown>} */ (data)
|
|
@@ -4865,6 +4896,22 @@ export function enabledApisValueFromPatchText(patchText) {
|
|
|
4865
4896
|
return typeof v === 'string' ? v : JSON.stringify(v)
|
|
4866
4897
|
}
|
|
4867
4898
|
|
|
4899
|
+
/**
|
|
4900
|
+
* Значення, яке inline-`patch` присвоює `data.HASURA_GRAPHQL_ENABLED_APIS`. Підтримка двох форматів:
|
|
4901
|
+
* **JSON6902** (`op` add/replace на `/data/HASURA_GRAPHQL_ENABLED_APIS`) і **Strategic Merge**
|
|
4902
|
+
* (`data.HASURA_GRAPHQL_ENABLED_APIS`). Зовнішні patch-файли (`patches[].path`) не охоплені — Plan B trade-off.
|
|
4903
|
+
* @param {string} patchText вміст поля `patch`
|
|
4904
|
+
* @returns {string | null} присвоєне значення (рядок) або null, якщо patch не чіпає цей ключ
|
|
4905
|
+
*/
|
|
4906
|
+
export function enabledApisValueFromPatchText(patchText) {
|
|
4907
|
+
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
4908
|
+
if (t === '') return null
|
|
4909
|
+
const parsed = firstValidYamlJsonFromPatchText(t)
|
|
4910
|
+
if (Array.isArray(parsed)) return enabledApisValueFromJson6902(parsed)
|
|
4911
|
+
if (parsed === null || typeof parsed !== 'object') return null
|
|
4912
|
+
return enabledApisValueFromStrategicMerge(parsed)
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4868
4915
|
/**
|
|
4869
4916
|
* Значення, яке `patches[]` kustomization присвоюють `data.HASURA_GRAPHQL_ENABLED_APIS` на цілі **ConfigMap**.
|
|
4870
4917
|
* Повертає значення першого patch-а, що чіпає цей ключ, або null, якщо такого немає.
|
|
@@ -6307,6 +6354,34 @@ function networkPolicyPodSelectorAppLabel(spec) {
|
|
|
6307
6354
|
return typeof app === 'string' ? app : ''
|
|
6308
6355
|
}
|
|
6309
6356
|
|
|
6357
|
+
/**
|
|
6358
|
+
* Витягує `kind` workload з анотації `nitra.dev/workload-kind` у `metadata.annotations`
|
|
6359
|
+
* NetworkPolicy-документа; дефолт `'Deployment'`, якщо анотації немає або вона порожня.
|
|
6360
|
+
* @param {Record<string, unknown>} docRec корінь NetworkPolicy-документа
|
|
6361
|
+
* @returns {string} workload-kind
|
|
6362
|
+
*/
|
|
6363
|
+
function legacyNetworkPolicyWorkloadKind(docRec) {
|
|
6364
|
+
const meta = getNestedObject(docRec, 'metadata')
|
|
6365
|
+
const annotations = meta === null ? null : getNestedObject(meta, 'annotations')
|
|
6366
|
+
const rawKind = annotations === null ? null : annotations['nitra.dev/workload-kind']
|
|
6367
|
+
return typeof rawKind === 'string' && rawKind !== '' ? rawKind : 'Deployment'
|
|
6368
|
+
}
|
|
6369
|
+
|
|
6370
|
+
/**
|
|
6371
|
+
* Будує `{ name, appLabel, kind }` для regenerate-канону з одного legacy NetworkPolicy-документа,
|
|
6372
|
+
* або `null`, якщо `name`/`appLabel` неповні (документ пропускається).
|
|
6373
|
+
* @param {unknown} doc розпарсений NetworkPolicy-документ
|
|
6374
|
+
* @returns {{ name: string, appLabel: string, kind: string } | null} spec або null
|
|
6375
|
+
*/
|
|
6376
|
+
function legacyNetworkPolicySpecFromDoc(doc) {
|
|
6377
|
+
const name = manifestMetadataName(doc)
|
|
6378
|
+
const docRec = /** @type {Record<string, unknown>} */ (doc)
|
|
6379
|
+
const appLabel = networkPolicyPodSelectorAppLabel(docRec.spec)
|
|
6380
|
+
const kind = legacyNetworkPolicyWorkloadKind(docRec)
|
|
6381
|
+
if (typeof name === 'string' && name !== '' && appLabel !== '') return { name, appLabel, kind }
|
|
6382
|
+
return null
|
|
6383
|
+
}
|
|
6384
|
+
|
|
6310
6385
|
/**
|
|
6311
6386
|
* Migrate legacy `networkpolicy.yaml`: якщо хоч один документ має catch-all in-cluster egress —
|
|
6312
6387
|
* перезаписати **всі** документи у файлі через `buildNetworkPolicyYaml(name, appLabel, kind, gclbPorts)`.
|
|
@@ -6327,21 +6402,8 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs, fail) {
|
|
|
6327
6402
|
*/
|
|
6328
6403
|
const specs = []
|
|
6329
6404
|
for (const doc of docs) {
|
|
6330
|
-
const
|
|
6331
|
-
|
|
6332
|
-
const spec = docRec.spec
|
|
6333
|
-
const appLabel = networkPolicyPodSelectorAppLabel(spec)
|
|
6334
|
-
const meta = docRec.metadata
|
|
6335
|
-
const annotations =
|
|
6336
|
-
meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
6337
|
-
? /** @type {Record<string, unknown>} */ (meta).annotations
|
|
6338
|
-
: null
|
|
6339
|
-
const rawKind =
|
|
6340
|
-
annotations !== null && typeof annotations === 'object' && !Array.isArray(annotations)
|
|
6341
|
-
? /** @type {Record<string, unknown>} */ (annotations)['nitra.dev/workload-kind']
|
|
6342
|
-
: null
|
|
6343
|
-
const kind = typeof rawKind === 'string' && rawKind !== '' ? rawKind : 'Deployment'
|
|
6344
|
-
if (typeof name === 'string' && name !== '' && appLabel !== '') specs.push({ name, appLabel, kind })
|
|
6405
|
+
const spec = legacyNetworkPolicySpecFromDoc(doc)
|
|
6406
|
+
if (spec !== null) specs.push(spec)
|
|
6345
6407
|
}
|
|
6346
6408
|
if (specs.length === 0) return false
|
|
6347
6409
|
const dir = dirname(npAbs)
|
|
@@ -41,6 +41,77 @@ function moduleJsDoc(source) {
|
|
|
41
41
|
return m ? m[0] : null
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Чи `.mjs`-файл, що не є тестом (`*.test.mjs`).
|
|
46
|
+
* @param {import('node:fs').Dirent} fileEntry запис каталогу
|
|
47
|
+
* @returns {boolean} true для звичайних source-файлів
|
|
48
|
+
*/
|
|
49
|
+
function isSourceMjs(fileEntry) {
|
|
50
|
+
return (
|
|
51
|
+
fileEntry.isFile() && fileEntry.name.endsWith('.mjs') && !fileEntry.name.endsWith('.test.mjs')
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Перевіряє один source-файл: якщо поряд є `docs/<stem>.md` і module-level JSDoc
|
|
57
|
+
* містить >1 непорожній рядок — репортить порушення.
|
|
58
|
+
* @param {string} jsDir каталог `js/`
|
|
59
|
+
* @param {import('node:fs').Dirent} fileEntry запис файлу
|
|
60
|
+
* @param {string} cwd корінь репозиторію
|
|
61
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
async function checkSourceFile(jsDir, fileEntry, cwd, reporter) {
|
|
65
|
+
const stem = basename(fileEntry.name, '.mjs')
|
|
66
|
+
const docsPath = join(jsDir, 'docs', `${stem}.md`)
|
|
67
|
+
if (!existsSync(docsPath)) return
|
|
68
|
+
|
|
69
|
+
const filePath = join(jsDir, fileEntry.name)
|
|
70
|
+
const source = await readFile(filePath, 'utf8')
|
|
71
|
+
const block = moduleJsDoc(source)
|
|
72
|
+
if (!block) return
|
|
73
|
+
|
|
74
|
+
const count = contentLineCount(block)
|
|
75
|
+
if (count > 1) {
|
|
76
|
+
reporter.fail(
|
|
77
|
+
`${filePath.slice(cwd.length + 1)}: docs/${stem}.md вже описує поведінку — module-level JSDoc має бути pointer (≤1 рядок, зараз ${count})`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Перевіряє всі source-файли в одному `js/`-каталозі правила/скіла.
|
|
84
|
+
* @param {string} jsDir каталог `js/`
|
|
85
|
+
* @param {string} cwd корінь репозиторію
|
|
86
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
89
|
+
async function checkJsDir(jsDir, cwd, reporter) {
|
|
90
|
+
for (const fileEntry of await readdir(jsDir, { withFileTypes: true })) {
|
|
91
|
+
if (!isSourceMjs(fileEntry)) continue
|
|
92
|
+
await checkSourceFile(jsDir, fileEntry, cwd, reporter)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Перевіряє один base-сегмент (`npm/rules` чи `npm/skills`): обходить піддиректорії
|
|
98
|
+
* правил/скілів і їхні `js/`-каталоги.
|
|
99
|
+
* @param {string} absBase абсолютний шлях до base-сегмента
|
|
100
|
+
* @param {string} cwd корінь репозиторію
|
|
101
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
*/
|
|
104
|
+
async function checkBaseSegment(absBase, cwd, reporter) {
|
|
105
|
+
for (const ruleEntry of await readdir(absBase, { withFileTypes: true })) {
|
|
106
|
+
if (!ruleEntry.isDirectory() || ruleEntry.name.startsWith('.')) continue
|
|
107
|
+
|
|
108
|
+
const jsDir = join(absBase, ruleEntry.name, 'js')
|
|
109
|
+
if (!existsSync(jsDir)) continue
|
|
110
|
+
|
|
111
|
+
await checkJsDir(jsDir, cwd, reporter)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
44
115
|
/**
|
|
45
116
|
* Сканує `npm/rules/*\/js/*.mjs` і `npm/skills/*\/js/*.mjs`.
|
|
46
117
|
* Якщо поряд існує `docs/<stem>.md` — module-level JSDoc має бути pointer (≤1 рядок),
|
|
@@ -54,33 +125,7 @@ export async function check(cwd = process.cwd()) {
|
|
|
54
125
|
for (const baseSegment of ['npm/rules', 'npm/skills']) {
|
|
55
126
|
const absBase = join(cwd, baseSegment)
|
|
56
127
|
if (!existsSync(absBase)) continue
|
|
57
|
-
|
|
58
|
-
for (const ruleEntry of await readdir(absBase, { withFileTypes: true })) {
|
|
59
|
-
if (!ruleEntry.isDirectory() || ruleEntry.name.startsWith('.')) continue
|
|
60
|
-
|
|
61
|
-
const jsDir = join(absBase, ruleEntry.name, 'js')
|
|
62
|
-
if (!existsSync(jsDir)) continue
|
|
63
|
-
|
|
64
|
-
for (const fileEntry of await readdir(jsDir, { withFileTypes: true })) {
|
|
65
|
-
if (!fileEntry.isFile() || !fileEntry.name.endsWith('.mjs') || fileEntry.name.endsWith('.test.mjs')) continue
|
|
66
|
-
|
|
67
|
-
const stem = basename(fileEntry.name, '.mjs')
|
|
68
|
-
const docsPath = join(jsDir, 'docs', `${stem}.md`)
|
|
69
|
-
if (!existsSync(docsPath)) continue
|
|
70
|
-
|
|
71
|
-
const filePath = join(jsDir, fileEntry.name)
|
|
72
|
-
const source = await readFile(filePath, 'utf8')
|
|
73
|
-
const block = moduleJsDoc(source)
|
|
74
|
-
if (!block) continue
|
|
75
|
-
|
|
76
|
-
const count = contentLineCount(block)
|
|
77
|
-
if (count > 1) {
|
|
78
|
-
reporter.fail(
|
|
79
|
-
`${filePath.slice(cwd.length + 1)}: docs/${stem}.md вже описує поведінку — module-level JSDoc має бути pointer (≤1 рядок, зараз ${count})`
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
128
|
+
await checkBaseSegment(absBase, cwd, reporter)
|
|
84
129
|
}
|
|
85
130
|
|
|
86
131
|
return reporter.getExitCode()
|