@nitra/cursor 5.2.1 → 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 +13 -0
- package/bin/n-cursor.js +72 -50
- package/lib/llm.mjs +60 -47
- package/lib/models.mjs +1 -1
- package/lib/omlx-trace.mjs +158 -0
- package/lib/omlx.mjs +49 -11
- package/package.json +1 -1
- package/rules/js-bun-db/js-bun-db.mdc +7 -7
- package/rules/js-lint/js-lint.mdc +14 -1
- package/rules/js-run/js-run.mdc +16 -16
- 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/rules/style-lint/js/tooling.mjs +13 -4
- package/rules/style-lint/style-lint.mdc +1 -1
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
- package/rules/test/js/data/stryker_config/stryker.config.vue.baseline.mjs +1 -1
- package/rules/test/js/stryker_config.mjs +33 -5
- package/rules/test/js/vitest-config-pool-forks.mjs +11 -7
- package/rules/test/test.mdc +9 -9
- package/rules/vue/vue.mdc +6 -6
- package/scripts/coverage-classify/index.mjs +5 -17
- 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/llm-worker.mjs +10 -22
- 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/rules/js-run/js-run.mdc
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
|
|
3
3
|
globs: "**/package.json,**/jsconfig.json,**/src/**/*.{js,mjs,cjs,ts,tsx}"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.12'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Область застосування
|
|
@@ -97,7 +97,7 @@ import sql from 'mssql'
|
|
|
97
97
|
import { GraphQLClient } from '@nitra/graphql-request'
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
то ці підключення повинні бути винесені в окремий файл, наприклад `/src/conn/pg.
|
|
100
|
+
то ці підключення повинні бути винесені в окремий файл, наприклад `/src/conn/pg.mjs`, в package.json повинні бути додано аліас:
|
|
101
101
|
|
|
102
102
|
```json
|
|
103
103
|
{
|
|
@@ -110,7 +110,7 @@ import { GraphQLClient } from '@nitra/graphql-request'
|
|
|
110
110
|
|
|
111
111
|
так виглядатиме підключення до PostgreSQL в коді:
|
|
112
112
|
|
|
113
|
-
```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.
|
|
113
|
+
```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.mjs"
|
|
114
114
|
import { checkEnv, env } from '@nitra/check-env'
|
|
115
115
|
import { SQL } from 'bun'
|
|
116
116
|
|
|
@@ -140,7 +140,7 @@ export const graphQLClientSmart = new GraphQLClient(env.QL, {
|
|
|
140
140
|
а в коді повинно бути використано:
|
|
141
141
|
|
|
142
142
|
```js
|
|
143
|
-
import { pool } from '#conn/pg.
|
|
143
|
+
import { pool } from '#conn/pg.mjs'
|
|
144
144
|
|
|
145
145
|
// або
|
|
146
146
|
|
|
@@ -152,24 +152,24 @@ import { gql, graphQLClient } from '@nitra/graphql-request'
|
|
|
152
152
|
Назва файла в `src/conn/` має одразу повідомляти, **до чого** підключаємось і **в якому режимі**:
|
|
153
153
|
|
|
154
154
|
- **GraphQL** — префікс `ql-`, далі ідентифікатор endpoint:
|
|
155
|
-
- `src/conn/ql-contract.
|
|
156
|
-
- `src/conn/ql-smart.
|
|
155
|
+
- `src/conn/ql-contract.mjs`
|
|
156
|
+
- `src/conn/ql-smart.mjs`
|
|
157
157
|
- **PostgreSQL** — префікс `pg-`, далі тип підключення (репліка vs мастер): `read` або `write`:
|
|
158
|
-
- `src/conn/pg-read.
|
|
159
|
-
- `src/conn/pg-write.
|
|
158
|
+
- `src/conn/pg-read.mjs`
|
|
159
|
+
- `src/conn/pg-write.mjs`
|
|
160
160
|
- **PostgreSQL до кількох БД** — додатково ідентифікатор підключення після типу:
|
|
161
|
-
- `src/conn/pg-read-smart.
|
|
162
|
-
- `src/conn/pg-write-contract.
|
|
163
|
-
- **MySQL** — префікс `mysql-` за тією ж схемою (`mysql-read.
|
|
164
|
-
- **MSSQL** — префікс `mssql-` за тією ж схемою (`mssql-read.
|
|
161
|
+
- `src/conn/pg-read-smart.mjs`
|
|
162
|
+
- `src/conn/pg-write-contract.mjs`
|
|
163
|
+
- **MySQL** — префікс `mysql-` за тією ж схемою (`mysql-read.mjs`, `mysql-write-<id>.mjs` тощо).
|
|
164
|
+
- **MSSQL** — префікс `mssql-` за тією ж схемою (`mssql-read.mjs`, `mssql-write-<id>.mjs` тощо). Хоча npm-пакет один (`mssql`), а драйвер MS SQL Server під капотом T-SQL — у файловій назві відрізняємо MS SQL Server від MySQL, бо це різні СУБД, різні діалекти, різні рантаймні залежності. Якщо проєкт історично використовує `mysql-…` для MSSQL-підключень — він валідний і далі (для backward-compat), але новий код пишемо з префіксом `mssql-`.
|
|
165
165
|
|
|
166
|
-
Підключення до БД **обов'язково** має бути ідентифіковано як `read` (репліка) або `write` (мастер). Якщо з імені змінної оточення (наприклад, `env.PG_CONN`) це не очевидно — визнач режим за операціями в коді: якщо немає операцій зміни даних (`INSERT`/`UPDATE`/`DELETE`/DDL) — це `pg-read.
|
|
166
|
+
Підключення до БД **обов'язково** має бути ідентифіковано як `read` (репліка) або `write` (мастер). Якщо з імені змінної оточення (наприклад, `env.PG_CONN`) це не очевидно — визнач режим за операціями в коді: якщо немає операцій зміни даних (`INSERT`/`UPDATE`/`DELETE`/DDL) — це `pg-read.mjs`, інакше `pg-write.mjs`.
|
|
167
167
|
|
|
168
168
|
### Експорти у файлах `src/conn/`
|
|
169
169
|
|
|
170
170
|
У файлах підключень **заборонений** `export default`. Експорт має бути **іменований** і збігатися з назвою файла в camelCase.
|
|
171
171
|
|
|
172
|
-
Приклад — `src/conn/ql-smart.
|
|
172
|
+
Приклад — `src/conn/ql-smart.mjs`:
|
|
173
173
|
|
|
174
174
|
```javascript title="❌ Так не можна"
|
|
175
175
|
export default new GraphQLClient(env.SMART_QL, {
|
|
@@ -187,13 +187,13 @@ export const qlSmart = new GraphQLClient(env.SMART_QL, {
|
|
|
187
187
|
})
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
Відповідно: `pg-read.
|
|
190
|
+
Відповідно: `pg-read.mjs` → `export const pgRead = …`, `pg-write-contract.mjs` → `export const pgWriteContract = …`, `ql-contract.mjs` → `export const qlContract = …`.
|
|
191
191
|
|
|
192
192
|
## CheckEnv
|
|
193
193
|
|
|
194
194
|
Усі змінні оточення, які використовуються в коді, повинні бути перевірені за допомогою `checkEnv` з пакету `@nitra/check-env`. Це гарантує, що всі необхідні змінні оточення встановлені перед запуском програми.
|
|
195
195
|
|
|
196
|
-
```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.
|
|
196
|
+
```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.mjs"
|
|
197
197
|
import { checkEnv, env } from '@nitra/check-env'
|
|
198
198
|
import { SQL } from 'bun'
|
|
199
199
|
|
|
@@ -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()
|
|
@@ -6,6 +6,77 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
|
6
6
|
import { parseRuleAutoSpec, parseRuleLintPhase, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
|
|
7
7
|
import { RULE_PREDICATES } from '../../../scripts/lib/rule-predicates.mjs'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Перевіряє поле `auto` у meta.json одного правила.
|
|
11
|
+
* @param {string} id ідентифікатор правила
|
|
12
|
+
* @param {Record<string, unknown>} raw сирий meta.json
|
|
13
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
14
|
+
* @returns {boolean} true, якщо поле валідне (або відсутнє)
|
|
15
|
+
*/
|
|
16
|
+
function checkAutoField(id, raw, reporter) {
|
|
17
|
+
if (raw.auto === undefined) return true
|
|
18
|
+
const spec = parseRuleAutoSpec(raw.auto)
|
|
19
|
+
if (spec === null) {
|
|
20
|
+
reporter.fail(`rules/${id}: meta.json.auto нерозпізнане (очікується "завжди" / масив / {glob} / {predicate})`)
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if ('predicate' in spec && !Object.hasOwn(RULE_PREDICATES, spec.predicate)) {
|
|
24
|
+
reporter.fail(`rules/${id}: невідомий predicate "${spec.predicate}" (немає в RULE_PREDICATES)`)
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Перевіряє поле `lint` у meta.json одного правила.
|
|
32
|
+
* @param {string} id ідентифікатор правила
|
|
33
|
+
* @param {string} ruleDir каталог правила
|
|
34
|
+
* @param {Record<string, unknown>} raw сирий meta.json
|
|
35
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
36
|
+
* @returns {boolean} true, якщо поле валідне (або відсутнє)
|
|
37
|
+
*/
|
|
38
|
+
function checkLintField(id, ruleDir, raw, reporter) {
|
|
39
|
+
if (raw.lint === undefined) return true
|
|
40
|
+
if (parseRuleLintPhase(raw.lint) === null) {
|
|
41
|
+
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
if (!existsSync(join(ruleDir, 'js', 'lint.mjs'))) {
|
|
45
|
+
reporter.fail(`rules/${id}: lint:"${raw.lint}" але немає js/lint.mjs`)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Валідує meta.json одного правила.
|
|
53
|
+
* @param {string} id ідентифікатор правила
|
|
54
|
+
* @param {string} ruleDir каталог правила
|
|
55
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function checkRule(id, ruleDir, reporter) {
|
|
59
|
+
let ruleOk = true
|
|
60
|
+
|
|
61
|
+
if (existsSync(join(ruleDir, 'auto.md'))) {
|
|
62
|
+
reporter.fail(`rules/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
|
|
63
|
+
ruleOk = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const raw = readRuleMetaRaw(ruleDir)
|
|
67
|
+
if (!raw) {
|
|
68
|
+
reporter.fail(`rules/${id}: відсутній або невалідний meta.json`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!checkAutoField(id, raw, reporter)) ruleOk = false
|
|
73
|
+
if (!checkLintField(id, ruleDir, raw, reporter)) ruleOk = false
|
|
74
|
+
|
|
75
|
+
if (ruleOk) {
|
|
76
|
+
reporter.pass(`rules/${id}: meta.json валідний`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
9
80
|
/**
|
|
10
81
|
* Валідує всі `npm/rules/<id>/meta.json`.
|
|
11
82
|
* @param {string} [cwd] корінь репозиторію
|
|
@@ -21,42 +92,7 @@ export function check(cwd = process.cwd()) {
|
|
|
21
92
|
|
|
22
93
|
for (const entry of readdirSync(rulesDir, { withFileTypes: true })) {
|
|
23
94
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
24
|
-
|
|
25
|
-
const ruleDir = join(rulesDir, id)
|
|
26
|
-
let ruleOk = true
|
|
27
|
-
|
|
28
|
-
if (existsSync(join(ruleDir, 'auto.md'))) {
|
|
29
|
-
reporter.fail(`rules/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
|
|
30
|
-
ruleOk = false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const raw = readRuleMetaRaw(ruleDir)
|
|
34
|
-
if (!raw) {
|
|
35
|
-
reporter.fail(`rules/${id}: відсутній або невалідний meta.json`)
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
38
|
-
if (raw.auto !== undefined) {
|
|
39
|
-
const spec = parseRuleAutoSpec(raw.auto)
|
|
40
|
-
if (spec === null) {
|
|
41
|
-
reporter.fail(`rules/${id}: meta.json.auto нерозпізнане (очікується "завжди" / масив / {glob} / {predicate})`)
|
|
42
|
-
ruleOk = false
|
|
43
|
-
} else if ('predicate' in spec && !Object.hasOwn(RULE_PREDICATES, spec.predicate)) {
|
|
44
|
-
reporter.fail(`rules/${id}: невідомий predicate "${spec.predicate}" (немає в RULE_PREDICATES)`)
|
|
45
|
-
ruleOk = false
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (raw.lint !== undefined) {
|
|
49
|
-
if (parseRuleLintPhase(raw.lint) === null) {
|
|
50
|
-
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
|
|
51
|
-
ruleOk = false
|
|
52
|
-
} else if (!existsSync(join(ruleDir, 'js', 'lint.mjs'))) {
|
|
53
|
-
reporter.fail(`rules/${id}: lint:"${raw.lint}" але немає js/lint.mjs`)
|
|
54
|
-
ruleOk = false
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (ruleOk) {
|
|
58
|
-
reporter.pass(`rules/${id}: meta.json валідний`)
|
|
59
|
-
}
|
|
95
|
+
checkRule(entry.name, join(rulesDir, entry.name), reporter)
|
|
60
96
|
}
|
|
61
97
|
|
|
62
98
|
return Promise.resolve(reporter.getExitCode())
|