@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bin/n-cursor.js +72 -50
  3. package/lib/llm.mjs +60 -47
  4. package/lib/models.mjs +1 -1
  5. package/lib/omlx-trace.mjs +158 -0
  6. package/lib/omlx.mjs +49 -11
  7. package/package.json +1 -1
  8. package/rules/js-bun-db/js-bun-db.mdc +7 -7
  9. package/rules/js-lint/js-lint.mdc +14 -1
  10. package/rules/js-run/js-run.mdc +16 -16
  11. package/rules/k8s/js/manifests.mjs +144 -82
  12. package/rules/npm-module/js/header_doc_pointer.mjs +72 -27
  13. package/rules/npm-module/js/rule_meta.mjs +72 -36
  14. package/rules/npm-module/js/skill_meta.mjs +59 -35
  15. package/rules/style-lint/js/tooling.mjs +13 -4
  16. package/rules/style-lint/style-lint.mdc +1 -1
  17. package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
  18. package/rules/test/js/data/stryker_config/stryker.config.vue.baseline.mjs +1 -1
  19. package/rules/test/js/stryker_config.mjs +33 -5
  20. package/rules/test/js/vitest-config-pool-forks.mjs +11 -7
  21. package/rules/test/test.mdc +9 -9
  22. package/rules/vue/vue.mdc +6 -6
  23. package/scripts/coverage-classify/index.mjs +5 -17
  24. package/scripts/coverage-classify/verdict-schema.mjs +1 -1
  25. package/scripts/lib/assert-project-root.mjs +1 -1
  26. package/scripts/lib/discover-check-rules-from-cursor.mjs +1 -1
  27. package/scripts/lib/rule-predicates.mjs +30 -18
  28. package/scripts/lib/run-rule-cli.mjs +1 -1
  29. package/scripts/lib/run-standard-rule.mjs +1 -1
  30. package/scripts/post-tool-use-fix.mjs +3 -3
  31. package/scripts/skills-cli.mjs +5 -5
  32. package/scripts/worktree-cli.mjs +5 -5
  33. package/skills/doc-files/js/docgen-extract.mjs +1 -1
  34. package/skills/doc-files/js/docgen-files-batch.mjs +65 -34
  35. package/skills/doc-files/js/docgen-gen.mjs +121 -36
  36. package/skills/doc-files/js/docgen-prompts.mjs +20 -5
  37. package/skills/fix/js/llm-worker.mjs +10 -22
  38. package/skills/fix/js/orchestrator.mjs +64 -35
  39. package/skills/fix/js/t0.mjs +44 -32
  40. package/skills/start-check/js/check.mjs +1 -1
@@ -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.11'
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.js`, в package.json повинні бути додано аліас:
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.js"
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.js'
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.js`
156
- - `src/conn/ql-smart.js`
155
+ - `src/conn/ql-contract.mjs`
156
+ - `src/conn/ql-smart.mjs`
157
157
  - **PostgreSQL** — префікс `pg-`, далі тип підключення (репліка vs мастер): `read` або `write`:
158
- - `src/conn/pg-read.js`
159
- - `src/conn/pg-write.js`
158
+ - `src/conn/pg-read.mjs`
159
+ - `src/conn/pg-write.mjs`
160
160
  - **PostgreSQL до кількох БД** — додатково ідентифікатор підключення після типу:
161
- - `src/conn/pg-read-smart.js`
162
- - `src/conn/pg-write-contract.js`
163
- - **MySQL** — префікс `mysql-` за тією ж схемою (`mysql-read.js`, `mysql-write-<id>.js` тощо).
164
- - **MSSQL** — префікс `mssql-` за тією ж схемою (`mssql-read.js`, `mssql-write-<id>.js` тощо). Хоча npm-пакет один (`mssql`), а драйвер MS SQL Server під капотом T-SQL — у файловій назві відрізняємо MS SQL Server від MySQL, бо це різні СУБД, різні діалекти, різні рантаймні залежності. Якщо проєкт історично використовує `mysql-…` для MSSQL-підключень — він валідний і далі (для backward-compat), але новий код пишемо з префіксом `mssql-`.
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.js`, інакше `pg-write.js`.
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.js`:
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.js` → `export const pgRead = …`, `pg-write-contract.js` → `export const pgWriteContract = …`, `ql-contract.js` → `export const qlContract = …`.
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.js"
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 {any} */ (parseDocument(raw).toJS()).spec
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 {ReadonlyArray<{ ipBlock: { cidr: string } }>}
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
- let raw
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
- * Значення, яке inline-`patch` присвоює `data.HASURA_GRAPHQL_ENABLED_APIS`. Підтримка двох форматів:
4828
- * **JSON6902** (`op` add/replace на `/data/HASURA_GRAPHQL_ENABLED_APIS`) і **Strategic Merge**
4829
- * (`data.HASURA_GRAPHQL_ENABLED_APIS`). Зовнішні patch-файли (`patches[].path`) не охоплені Plan B trade-off.
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
- export function enabledApisValueFromPatchText(patchText) {
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(t)) {
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 null
4861
+ return undefined
4846
4862
  }
4847
- if (Array.isArray(parsed)) {
4848
- for (const item of parsed) {
4849
- if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
4850
- const rec = /** @type {Record<string, unknown>} */ (item)
4851
- const op = typeof rec.op === 'string' ? rec.op.trim().toLowerCase() : ''
4852
- const path = typeof rec.path === 'string' ? normalizeJsonPatchPath(rec.path) : ''
4853
- if ((op === 'add' || op === 'replace') && path === HASURA_ENABLED_APIS_DATA_POINTER) {
4854
- return typeof rec.value === 'string' ? rec.value : JSON.stringify(rec.value)
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
- if (parsed === null || typeof parsed !== 'object') return null
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 name = manifestMetadataName(doc)
6331
- const docRec = /** @type {Record<string, unknown>} */ (doc)
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
- const id = entry.name
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())