@nitra/cursor 1.8.220 → 1.8.222

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 (51) hide show
  1. package/.claude-template/npm-CLAUDE.md +4 -0
  2. package/CHANGELOG.md +21 -0
  3. package/bin/auto-rules.md +2 -0
  4. package/bin/n-cursor.js +25 -4
  5. package/mdc/ci4.mdc +51 -0
  6. package/mdc/tauri.mdc +20 -0
  7. package/package.json +1 -1
  8. package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
  9. package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
  10. package/policy/k8s/base_manifest/base_manifest.rego +154 -0
  11. package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
  12. package/policy/k8s/gateway/gateway.rego +151 -0
  13. package/policy/k8s/gateway/gateway_test.rego +122 -0
  14. package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
  15. package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
  16. package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
  17. package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
  18. package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
  19. package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
  20. package/policy/k8s/kustomization/kustomization.rego +220 -0
  21. package/policy/k8s/kustomization/kustomization_test.rego +128 -0
  22. package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
  23. package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
  24. package/policy/k8s/manifest/manifest.rego +151 -4
  25. package/policy/k8s/manifest/manifest_test.rego +309 -0
  26. package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
  27. package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
  28. package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
  29. package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
  30. package/scripts/auto-skills.mjs +8 -1
  31. package/scripts/check-bun.mjs +3 -3
  32. package/scripts/check-changelog.mjs +2 -3
  33. package/scripts/check-image-avif.mjs +14 -6
  34. package/scripts/check-image-compress.mjs +1 -1
  35. package/scripts/check-js-run.mjs +58 -47
  36. package/scripts/check-k8s.mjs +128 -51
  37. package/scripts/check-npm-module.mjs +1 -4
  38. package/scripts/check-php.mjs +5 -5
  39. package/scripts/claude-stop-hook.mjs +2 -2
  40. package/scripts/lint-conftest.mjs +88 -8
  41. package/scripts/lint-ga.mjs +1 -1
  42. package/scripts/lint-rego.mjs +19 -4
  43. package/scripts/run-shellcheck-text.mjs +94 -64
  44. package/scripts/sync-claude-config.mjs +1 -1
  45. package/scripts/utils/ast-scan-utils.mjs +28 -0
  46. package/scripts/utils/bun-sql-scan.mjs +53 -34
  47. package/scripts/utils/bunyan-imports.mjs +10 -61
  48. package/scripts/utils/conn-file-rules.mjs +76 -37
  49. package/scripts/utils/depcheck-workflow.mjs +27 -6
  50. package/scripts/utils/redis-imports.mjs +9 -51
  51. package/skills/llm-patch/SKILL.md +16 -5
@@ -19,13 +19,16 @@ import { parseProgramOrNull } from './ast-scan-utils.mjs'
19
19
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
20
20
 
21
21
  /**
22
- * Канонічний шаблон імені файла в каталозі conn.
23
- * - `ql-<id>` для GraphQL;
24
- * - `(pg|mysql|mssql)-(read|write)(-<id>)?` для БД.
25
- * `<id>` — починається з [a-z0-9], далі [a-z0-9-]*.
22
+ * Канонічний шаблон імені GraphQL-файла: `ql-<id>.<ext>`.
23
+ * `<id>` kebab без leading/trailing-`-`, починається/закінчується на `[a-z0-9]`.
26
24
  */
27
- const CONN_FILENAME_RE =
28
- /^(?:ql-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|(?:pg|mysql|mssql)-(?:read|write)(?:-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)?)\.([cm]?[jt]sx?)$/u
25
+ const CONN_FILENAME_QL_RE = /^ql-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.[cm]?[jt]sx?$/u
26
+ /**
27
+ * Канонічний шаблон імені файла БД-підключення: `(pg|mysql|mssql)-(read|write)(-<id>)?.<ext>`.
28
+ * `<id>` — за тими ж правилами, що й для `ql-`. Розділили з GraphQL-формою, щоб
29
+ * не множити комплексність regex (sonarjs/regex-complexity).
30
+ */
31
+ const CONN_FILENAME_DB_RE = /^(?:pg|mysql|mssql)-(?:read|write)(?:-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)?\.[cm]?[jt]sx?$/u
29
32
 
30
33
  /**
31
34
  * Чи це файл, який сканується правилом «conn-file» (JS/TS-сімʼя, без `.d.ts`).
@@ -43,7 +46,7 @@ export function isConnFileRulesSourceFile(relativePathPosix) {
43
46
  */
44
47
  function basenameNoExt(relativePathPosix) {
45
48
  const last = relativePathPosix.lastIndexOf('/')
46
- const base = last >= 0 ? relativePathPosix.slice(last + 1) : relativePathPosix
49
+ const base = last === -1 ? relativePathPosix : relativePathPosix.slice(last + 1)
47
50
  const dot = base.lastIndexOf('.')
48
51
  return dot > 0 ? base.slice(0, dot) : base
49
52
  }
@@ -64,8 +67,71 @@ export function kebabToCamel(kebab) {
64
67
  */
65
68
  export function isConnFileNameValid(relativePathPosix) {
66
69
  const last = relativePathPosix.lastIndexOf('/')
67
- const base = last >= 0 ? relativePathPosix.slice(last + 1) : relativePathPosix
68
- return CONN_FILENAME_RE.test(base)
70
+ const base = last === -1 ? relativePathPosix : relativePathPosix.slice(last + 1)
71
+ return CONN_FILENAME_QL_RE.test(base) || CONN_FILENAME_DB_RE.test(base)
72
+ }
73
+
74
+ /**
75
+ * Витягує імена з `export const/let/var X = …` (включно з кількома declarators у одному `export const a, b`).
76
+ * @param {Record<string, unknown>} decl AST `VariableDeclaration`
77
+ * @returns {string[]} імена змінних
78
+ */
79
+ function namesFromVariableDeclaration(decl) {
80
+ if (!Array.isArray(decl.declarations)) return []
81
+ /** @type {string[]} */
82
+ const out = []
83
+ for (const d of decl.declarations) {
84
+ const id = /** @type {Record<string, unknown> | null} */ (d?.id ?? null)
85
+ if (id && id.type === 'Identifier' && typeof id.name === 'string') out.push(id.name)
86
+ }
87
+ return out
88
+ }
89
+
90
+ /**
91
+ * Витягує імʼя з `export function X` / `export class X`.
92
+ * @param {Record<string, unknown>} decl AST `FunctionDeclaration` або `ClassDeclaration`
93
+ * @returns {string | null} імʼя або `null`, якщо id-вузол анонімний
94
+ */
95
+ function nameFromFnOrClassDeclaration(decl) {
96
+ if (decl.type !== 'FunctionDeclaration' && decl.type !== 'ClassDeclaration') return null
97
+ const id = /** @type {Record<string, unknown> | null} */ (decl.id ?? null)
98
+ if (!id || typeof id !== 'object') return null
99
+ return typeof id.name === 'string' ? id.name : null
100
+ }
101
+
102
+ /**
103
+ * Витягує експортоване імʼя з одного `ExportSpecifier` (`export { X }` / `export { X as Y }`).
104
+ * @param {Record<string, unknown> | null | undefined} specifier AST `ExportSpecifier`
105
+ * @returns {string | null} імʼя або `null`
106
+ */
107
+ function nameFromExportSpecifier(specifier) {
108
+ const exported = /** @type {Record<string, unknown> | null} */ (specifier?.exported ?? null)
109
+ if (!exported) return null
110
+ if (exported.type === 'Identifier' && typeof exported.name === 'string') return exported.name
111
+ if (typeof exported.value === 'string') return exported.value
112
+ return null
113
+ }
114
+
115
+ /**
116
+ * Імена з одного `ExportNamedDeclaration` — або з вкладеного `declaration`, або зі списку `specifiers`.
117
+ * @param {Record<string, unknown>} rec AST `ExportNamedDeclaration`
118
+ * @returns {string[]} імена цього експортного вузла
119
+ */
120
+ function namesFromNamedExport(rec) {
121
+ const decl = /** @type {Record<string, unknown> | null} */ (rec.declaration ?? null)
122
+ if (decl) {
123
+ if (decl.type === 'VariableDeclaration') return namesFromVariableDeclaration(decl)
124
+ const fnOrClass = nameFromFnOrClassDeclaration(decl)
125
+ return fnOrClass ? [fnOrClass] : []
126
+ }
127
+ if (!Array.isArray(rec.specifiers)) return []
128
+ /** @type {string[]} */
129
+ const out = []
130
+ for (const s of rec.specifiers) {
131
+ const name = nameFromExportSpecifier(/** @type {Record<string, unknown> | null} */ (s ?? null))
132
+ if (name) out.push(name)
133
+ }
134
+ return out
69
135
  }
70
136
 
71
137
  /**
@@ -87,34 +153,7 @@ function collectNamedExportNames(program) {
87
153
  if (!node || typeof node !== 'object') continue
88
154
  const rec = /** @type {Record<string, unknown>} */ (node)
89
155
  if (rec.type !== 'ExportNamedDeclaration') continue
90
- const decl = /** @type {Record<string, unknown> | null} */ (rec.declaration ?? null)
91
- if (decl) {
92
- // export const X = ... / export let / export var
93
- if (decl.type === 'VariableDeclaration' && Array.isArray(decl.declarations)) {
94
- for (const d of decl.declarations) {
95
- const id = /** @type {Record<string, unknown> | null} */ (d?.id ?? null)
96
- if (id && id.type === 'Identifier' && typeof id.name === 'string') out.push(id.name)
97
- }
98
- }
99
- // export function X / export class X
100
- if (
101
- (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&
102
- decl.id &&
103
- typeof decl.id === 'object' &&
104
- typeof (/** @type {Record<string, unknown>} */ (decl.id).name) === 'string'
105
- ) {
106
- out.push(/** @type {string} */ (/** @type {Record<string, unknown>} */ (decl.id).name))
107
- }
108
- } else if (Array.isArray(rec.specifiers)) {
109
- // export { X } / export { X as Y }
110
- for (const s of rec.specifiers) {
111
- const exported = /** @type {Record<string, unknown> | null} */ (s?.exported ?? null)
112
- if (!exported) continue
113
- // ESTree: Identifier (name) або Literal (value), залежно від спеки
114
- if (exported.type === 'Identifier' && typeof exported.name === 'string') out.push(exported.name)
115
- else if (typeof exported.value === 'string') out.push(exported.value)
116
- }
117
- }
156
+ out.push(...namesFromNamedExport(rec))
118
157
  }
119
158
  return out
120
159
  }
@@ -23,8 +23,31 @@ import { flattenWorkflowSteps, getStepRun, parseWorkflowYaml } from './gha-workf
23
23
 
24
24
  const WORKFLOWS_DIR_REL = '.github/workflows'
25
25
  const REQUIRED_IGNORES = ['graphql', 'bun']
26
- const DEPCHECK_RUN_RE = /(?:^|[\s;&|])npx\s+depcheck\b([^\n]*)/u
27
- const IGNORES_FLAG_RE = /--ignores\s*=?\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
26
+ // `npx depcheck` як ціла команда у одному рядку shell-скрипту.
27
+ // `[^\n]*` обмежено явним `\n`-stop'ом — `*` не може backtrack-нутися за межі рядка.
28
+ const DEPCHECK_RUN_RE = /(?:^|[\s;&|])npx[ \t]+depcheck\b([^\n]*)/u
29
+ // `--ignores=…` або `--ignores …` з трьома формами значення (двійкові, одинарні, без лапок).
30
+ // Розділювач — або `=` з опційними пробілами, або один+ пробіл. Альтернативи значення
31
+ // не перетинаються (стартують з різних символів), тож backtrack-у між ними нема.
32
+ const IGNORES_FLAG_RE = /--ignores(?:=[ \t]*|[ \t]+)(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
33
+
34
+ /**
35
+ * Нормалізує шлях: бекслеші → forward, обрізає trailing-слеші. Без regex-у на trailing,
36
+ * щоб не тригерити `sonarjs/slow-regex` на `\/+$`.
37
+ * @param {string} p вхідний шлях
38
+ * @returns {string} нормалізований шлях
39
+ */
40
+ function normalizePath(p) {
41
+ let end = p.length
42
+ while (end > 0) {
43
+ const cp = p.codePointAt(end - 1)
44
+ if (cp !== 47 && cp !== 92) break
45
+ end--
46
+ }
47
+ let out = end === p.length ? p : p.slice(0, end)
48
+ if (out.includes('\\')) out = out.replaceAll('\\', '/')
49
+ return out
50
+ }
28
51
 
29
52
  /**
30
53
  * Чи містить workflow.on[event].paths хоча б один patten, що починається з `<pkgRoot>/`.
@@ -33,7 +56,7 @@ const IGNORES_FLAG_RE = /--ignores\s*=?\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
33
56
  * @returns {boolean} `true`, якщо знайдено хоча б один підходящий glob
34
57
  */
35
58
  export function workflowHasPathsScopedToPackage(root, pkgRoot) {
36
- const prefix = `${pkgRoot.replaceAll('\\', '/').replace(/\/+$/, '')}/`
59
+ const prefix = `${normalizePath(pkgRoot)}/`
37
60
  const on = root?.on
38
61
  if (!on || typeof on !== 'object') return false
39
62
  for (const event of /** @type {const} */ (['push', 'pull_request'])) {
@@ -81,9 +104,7 @@ export function extractDepcheckArgs(runText) {
81
104
  export function stepWorkingDirectoryEquals(step, pkgRoot) {
82
105
  const wd = step['working-directory']
83
106
  if (typeof wd !== 'string') return false
84
- const norm = wd.replaceAll('\\', '/').replace(/\/+$/, '')
85
- const expected = pkgRoot.replaceAll('\\', '/').replace(/\/+$/, '')
86
- return norm === expected
107
+ return normalizePath(wd) === normalizePath(pkgRoot)
87
108
  }
88
109
 
89
110
  /**
@@ -19,7 +19,14 @@
19
19
  */
20
20
  import { parseSync } from 'oxc-parser'
21
21
 
22
- import { langFromPath, normalizeSnippet, offsetToLine } from './ast-scan-utils.mjs'
22
+ import {
23
+ dynamicImportModule,
24
+ langFromPath,
25
+ normalizeSnippet,
26
+ offsetToLine,
27
+ requireCallModule,
28
+ walkAstWithAncestors
29
+ } from './ast-scan-utils.mjs'
23
30
 
24
31
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
25
32
  const FORBIDDEN_MODULE_NAMES = new Set([
@@ -48,55 +55,6 @@ function isForbiddenRedisModule(mod) {
48
55
  return mod.startsWith('ioredis/') || mod.startsWith('redis/') || mod.startsWith('@redis/')
49
56
  }
50
57
 
51
- /**
52
- * Перевіряє, чи це виклик `require('<module>')` з рядковим аргументом.
53
- * @param {Record<string, unknown> | null | undefined} node вузол AST
54
- * @returns {string | null} ім'я модуля з аргументу, інакше `null`
55
- */
56
- function requireCallModule(node) {
57
- if (!node || node.type !== 'CallExpression') return null
58
- const callee = node.callee
59
- if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') return null
60
- const arg = node.arguments?.[0]
61
- if (!arg || arg.type !== 'Literal' || typeof arg.value !== 'string') return null
62
- return arg.value
63
- }
64
-
65
- /**
66
- * Перевіряє, чи це динамічний `import('<module>')` з рядковим аргументом.
67
- * @param {Record<string, unknown> | null | undefined} node вузол AST
68
- * @returns {string | null} ім'я модуля, інакше `null`
69
- */
70
- function dynamicImportModule(node) {
71
- if (!node || node.type !== 'ImportExpression') return null
72
- const src = node.source
73
- if (!src || src.type !== 'Literal' || typeof src.value !== 'string') return null
74
- return src.value
75
- }
76
-
77
- /**
78
- * Простий рекурсивний обхід AST: заходимо в усі обʼєкти/масиви, щоб знайти require/import-вузли.
79
- * @param {unknown} node корінь або під-вузол AST
80
- * @param {(n: unknown) => void} visit виклик для кожного обʼєкта-вузла
81
- * @returns {void}
82
- */
83
- function walkAst(node, visit) {
84
- if (!node || typeof node !== 'object') return
85
- if (Array.isArray(node)) {
86
- for (const item of node) walkAst(item, visit)
87
- return
88
- }
89
- if (typeof node.type === 'string') {
90
- visit(node)
91
- }
92
- for (const key of Object.keys(node)) {
93
- if (key !== 'parent') {
94
- const v = node[key]
95
- if (v && typeof v === 'object') walkAst(v, visit)
96
- }
97
- }
98
- }
99
-
100
58
  /**
101
59
  * Знаходить заборонені імпорти/require з `ioredis` / `node-redis` у тексті.
102
60
  * @param {string} content вихідний код
@@ -130,7 +88,7 @@ export function findRedisImportsInText(content, virtualPath = 'scan.ts') {
130
88
  }
131
89
  }
132
90
 
133
- walkAst(result.program, node => {
91
+ walkAstWithAncestors(result.program, [], node => {
134
92
  const reqMod = requireCallModule(node)
135
93
  if (reqMod && isForbiddenRedisModule(reqMod)) {
136
94
  out.push({
@@ -5,6 +5,10 @@ description: >-
5
5
  read-only аналіз CWD без жодних змін у поточному репо
6
6
  ---
7
7
 
8
+ <!-- markdownlint-disable-file MD024 MD025 -->
9
+ <!-- Файл демонструє шаблон промпта з кількома H1 (`# Завдання`, `# Релевантні файли` тощо)
10
+ — це інтенціональна частина showcase, а не порушення one-title-per-document. -->
11
+
8
12
  # Підготовка LLM-патчу (текстова комунікація між агентами)
9
13
 
10
14
  Скіл готує **самодостатній текстовий промпт** ("патч") для іншої LLM-сесії
@@ -83,16 +87,16 @@ description: >-
83
87
  - Документи правил: <CLAUDE.md / .cursor/rules — або "немає">
84
88
 
85
89
  ## Структура (skim)
86
-
87
90
  ```
91
+
88
92
  <вивід tree -L 2>
89
- ```
93
+ ````
90
94
 
91
95
  # Релевантні файли
92
96
 
93
97
  ## `package.json`
94
98
 
95
- ```json
99
+ ```text
96
100
  <повний вміст або ключові поля>
97
101
  ```
98
102
 
@@ -119,7 +123,11 @@ description: >-
119
123
  - `<команда з scripts — npm test / bun test / lint>`
120
124
  - <конкретні acceptance-checks: "у `engines.node` має бути `>=25`",
121
125
  "CI зелений" тощо>
126
+
127
+ ```
128
+
122
129
  ```
130
+
123
131
  ````
124
132
 
125
133
  ## Правила
@@ -158,7 +166,8 @@ description: >-
158
166
  Очікуваний вивід (схематично):
159
167
 
160
168
  ````
161
- ```markdown
169
+
170
+ ````markdown
162
171
  # Завдання
163
172
 
164
173
  Підняти `engines.node` у `@nitra/eslint-config` до `>=25` і переглянути
@@ -182,6 +191,7 @@ description: >-
182
191
  ```json
183
192
  { "engines": { "node": ">=22" }, "peerDependencies": { "eslint": "^9" } }
184
193
  ```
194
+ ````
185
195
 
186
196
  # Що треба зробити
187
197
 
@@ -193,6 +203,7 @@ description: >-
193
203
 
194
204
  - `bun test`
195
205
  - `node -v` у CI ≥ 25
206
+
196
207
  ```
197
208
  готово до копіювання — встав у чат з агентом у цільовому проєкті
198
- ````
209
+ ```