@nitra/cursor 1.8.147 → 1.8.150

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/bin/n-cursor.js CHANGED
@@ -561,7 +561,7 @@ function buildClaudeLintParallelismSectionLines() {
561
561
  '## Лінт і ESLint (без паралельних запусків)',
562
562
  '',
563
563
  'Щоб не запускати **кілька** одночасних **`eslint`** (і не перевантажувати диск/CPU), **заборонено** стартувати `bun run lint` / `lint-js` / `eslint` **паралельно** в різних Bash-задачах, **фонових** shells чи **субагентах** (Task тощо). Має бути **один** послідовний прогон на сесію; команда **`/n-lint`** — **не** ділити на паралельні підзадачі. Деталі: `.cursor/skills/n-lint/SKILL.md`.',
564
- '',
564
+ ''
565
565
  ]
566
566
  }
567
567
 
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * CLI для перейменування розширень YAML (k8s та `.github`). Бізнес-логіка — у **`scripts/rename-yaml-extensions.mjs`**.
4
3
  *
package/mdc/docker.mdc CHANGED
@@ -189,8 +189,10 @@ jobs:
189
189
  ```yaml title=".hadolint.yaml"
190
190
  ignored:
191
191
  - DL3007
192
+ - DL3008
192
193
  - DL3018
193
194
  ```
195
+
194
196
  Де DL3007 - «Не використовуй тег latest у FROM»
195
197
  Де DL3018 - «Піни версії пакетів у apk add»
196
198
 
package/mdc/js-mssql.mdc CHANGED
@@ -17,6 +17,7 @@ version: '1.2'
17
17
  ## Як виконувати запити (безпечно)
18
18
 
19
19
  tagged template треба викликати на request-обʼєкті цього пулу:
20
+
20
21
  ```javascript
21
22
  javascript// db.js
22
23
  import sql from 'mssql';
@@ -58,6 +59,7 @@ const result = await pool.request().query`
58
59
  ### Не робити `query(\`...\`)`
59
60
 
60
61
  javascript// ❌ Це не tagged template — це конкатенація рядка перед викликом
62
+
61
63
  ```javascript
62
64
  await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
63
65
  // ↑ круглі дужки замість бекті́ків = звичайна інтерполяція = SQL injection
package/mdc/k8s.mdc CHANGED
@@ -314,7 +314,7 @@ data:
314
314
 
315
315
  ### Прод-оверрайди у `kustomization.yaml`
316
316
 
317
- Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення:
317
+ Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення **лише якщо** цей оверлей наслідує base-дерево, де є **Deployment** і **HPA/PDB** (тобто base реально дає dev-like HPA/PDB, які треба підняти в проді):
318
318
 
319
319
  - для `HorizontalPodAutoscaler`: `spec.minReplicas` **і** `spec.maxReplicas` (щоб у проді вийшло ≥2).
320
320
  - для `PodDisruptionBudget`: `spec.minAvailable` (щоб у проді вийшло ≥1).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.147",
3
+ "version": "1.8.150",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -376,9 +376,7 @@ function extractNitraObjectBodySource(source) {
376
376
  * @returns {boolean} **true**, якщо в тілі є **iosCocoaPods**…**:** **true**
377
377
  */
378
378
  function nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody) {
379
- return (
380
- RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
381
- )
379
+ return RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
382
380
  }
383
381
 
384
382
  /**
@@ -443,7 +441,9 @@ export async function check() {
443
441
  const { byPath, anyCapacitor } = acc
444
442
 
445
443
  if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
446
- pass('Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено')
444
+ pass(
445
+ 'Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено'
446
+ )
447
447
  return getExitCode()
448
448
  }
449
449
 
@@ -18,7 +18,11 @@ import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-wo
18
18
  import { createCheckReporter } from './utils/check-reporter.mjs'
19
19
 
20
20
  /** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
21
- export const OXLINT_CANONICAL_JSON_PATH = join(dirname(fileURLToPath(import.meta.url)), 'utils', 'oxlint-canonical.json')
21
+ export const OXLINT_CANONICAL_JSON_PATH = join(
22
+ dirname(fileURLToPath(import.meta.url)),
23
+ 'utils',
24
+ 'oxlint-canonical.json'
25
+ )
22
26
 
23
27
  /** Очікуваний локальний скрипт. */
24
28
  export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
@@ -292,4 +292,3 @@ export async function check() {
292
292
 
293
293
  return reporter.getExitCode()
294
294
  }
295
-
@@ -4803,26 +4803,59 @@ function checkProdOverridesInKustomization(kust, rel, fail, passFn) {
4803
4803
  }
4804
4804
  }
4805
4805
 
4806
+ /**
4807
+ * Чи прод-оверлей **реально потребує** overrides для HPA/PDB.
4808
+ *
4809
+ * Overrides потрібні лише якщо оверлей (non-dev-like) посилається на `…/k8s/…/base` і у **base**-дереві
4810
+ * одночасно є:
4811
+ * - `Deployment` (у шарі `…/k8s/…/base/`), і
4812
+ * - `HorizontalPodAutoscaler` і/або `PodDisruptionBudget`.
4813
+ *
4814
+ * Тоді base зазвичай тримає dev-like значення (`1`/`1`/`0`), і прод-оверлей має їх підняти (див. k8s.mdc).
4815
+ * @param {string} rootNorm нормалізований корінь репозиторію
4816
+ * @param {string} kustAbs абсолютний шлях до kustomization.yaml
4817
+ * @returns {Promise<boolean>} true якщо потрібні overrides, інакше false
4818
+ */
4819
+ export async function prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs) {
4820
+ const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
4821
+ const segment = k8sEnvSegmentFromRelPath(rel)
4822
+ if (segment === null || isDevLikeK8sEnvSegment(segment)) return false
4823
+
4824
+ const kust = await readFirstYamlObject(kustAbs)
4825
+ if (kust === null) return false
4826
+
4827
+ const kustDir = dirname(kustAbs)
4828
+ const pathRefs = resourcePathRefsFromKustomizationObject(kust)
4829
+ const baseDirs = await k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootNorm)
4830
+ if (baseDirs.length === 0) return false
4831
+
4832
+ const flags = await Promise.all(
4833
+ baseDirs.map(bd => kustomizeResourceTreeHpaPdbDeploymentFlags(join(bd, 'kustomization.yaml'), rootNorm))
4834
+ )
4835
+
4836
+ return flags.some(f => f.hasDeployment && (f.hasHpa || f.hasPdb))
4837
+ }
4838
+
4806
4839
  /**
4807
4840
  * Для прод kustomization.yaml вимагає patches, що перевизначають **`/spec/minReplicas`** і **`/spec/maxReplicas`**
4808
- * на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**. Не застосовується
4809
- * до dev-like (base / dev / *-qa) — там ці значення беруть з base (див. k8s.mdc).
4841
+ * на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**.
4842
+ *
4843
+ * Не застосовується до dev-like (base / dev / *-qa).
4844
+ *
4845
+ * Також **не застосовується**, якщо оверлей не наслідує base з Deployment + HPA/PDB (див. `prodOverlayNeedsHpaPdbOverrides`).
4810
4846
  * @param {string} root корінь репозиторію
4811
4847
  * @param {string[]} yamlFilesAbs yaml під k8s
4812
4848
  * @param {(msg: string) => void} fail callback при помилці
4813
4849
  * @param {(msg: string) => void} passFn callback при успіху
4814
4850
  */
4815
4851
  async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, passFn) {
4852
+ const rootNorm = resolve(root)
4816
4853
  const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
4817
4854
  for (const kustAbs of kustFiles) {
4818
- const rel = relative(root, kustAbs).replaceAll('\\', '/')
4819
- const segment = k8sEnvSegmentFromRelPath(rel)
4820
- if (segment !== null && !isDevLikeK8sEnvSegment(segment)) {
4821
- const kust = await readFirstYamlObject(kustAbs)
4822
- if (kust !== null) {
4823
- checkProdOverridesInKustomization(kust, rel, fail, passFn)
4824
- }
4825
- }
4855
+ const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
4856
+ if (!(await prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs))) continue
4857
+ const kust = await readFirstYamlObject(kustAbs)
4858
+ if (kust !== null) checkProdOverridesInKustomization(kust, rel, fail, passFn)
4826
4859
  }
4827
4860
  }
4828
4861
 
@@ -77,4 +77,3 @@ export async function check() {
77
77
  await checkWorkflow(reporter)
78
78
  return reporter.getExitCode()
79
79
  }
80
-
@@ -40,7 +40,7 @@ const CSPELL_REQUIRED_IGNORE_PATHS = [
40
40
  '.vscode',
41
41
  'report',
42
42
  '*.svg',
43
- '**/k8s/**/*.yaml',
43
+ '**/k8s/**/*.yaml'
44
44
  ]
45
45
 
46
46
  /**
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Спільні утиліти для AST-сканерів JS/TS на oxc-parser:
3
+ * вибір мови за розширенням, переклад зміщення в номер рядка, стиснення сніпета,
4
+ * обхід AST з предками, парсинг програми з безпечним поверненням `null`,
5
+ * розпізнавання типових вузлів (функцій, `*.join(...)`),
6
+ * робота з `TemplateLiteral` (текст quasis, контекст SQL-списку).
7
+ *
8
+ * Використовується файлами `bun-sql-scan.mjs`, `mssql-pool-scan.mjs` та іншими сканерами
9
+ * для уникнення дублювання boilerplate.
10
+ */
11
+ import { parseSync } from 'oxc-parser'
12
+
13
+ const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
14
+
15
+ /**
16
+ * Мова для Oxc за шляхом файлу (розширення).
17
+ * @param {string} filePath віртуальний або реальний шлях до файлу
18
+ * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
19
+ */
20
+ export function langFromPath(filePath) {
21
+ const lower = filePath.toLowerCase()
22
+ if (lower.endsWith('.tsx')) return 'tsx'
23
+ if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
24
+ if (lower.endsWith('.jsx')) return 'jsx'
25
+ return 'js'
26
+ }
27
+
28
+ /**
29
+ * Номер рядка (1-based) за зміщенням у буфері.
30
+ * @param {string} content повний текст файлу
31
+ * @param {number} offset байтове зміщення початку фрагмента
32
+ * @returns {number} номер рядка від 1
33
+ */
34
+ export function offsetToLine(content, offset) {
35
+ let line = 1
36
+ const n = Math.min(offset, content.length)
37
+ for (let i = 0; i < n; i++) {
38
+ if (content.codePointAt(i) === 10) line++
39
+ }
40
+ return line
41
+ }
42
+
43
+ /**
44
+ * Стискає пробіли для повідомлення про порушення.
45
+ * @param {string} s фрагмент коду
46
+ * @returns {string} скорочений однорядковий рядок
47
+ */
48
+ export function normalizeSnippet(s) {
49
+ return s.replaceAll(/\s+/gu, ' ').trim().slice(0, 180)
50
+ }
51
+
52
+ /**
53
+ * Чи є вузол функцією.
54
+ * @param {unknown} node AST node
55
+ * @returns {boolean} true, якщо це будь-який вузол-функція
56
+ */
57
+ export function isFunctionNode(node) {
58
+ return (
59
+ !!node &&
60
+ typeof node === 'object' &&
61
+ typeof node.type === 'string' &&
62
+ (node.type === 'FunctionDeclaration' ||
63
+ node.type === 'FunctionExpression' ||
64
+ node.type === 'ArrowFunctionExpression')
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
70
+ * @param {unknown} node поточний вузол
71
+ * @param {unknown[]} ancestors масив предків від кореня до parent
72
+ * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
73
+ * @returns {void}
74
+ */
75
+ export function walkAstWithAncestors(node, ancestors, visit) {
76
+ if (!node || typeof node !== 'object') return
77
+ if (Array.isArray(node)) {
78
+ for (const item of node) walkAstWithAncestors(item, ancestors, visit)
79
+ return
80
+ }
81
+
82
+ const rec = /** @type {Record<string, unknown>} */ (node)
83
+ if (typeof rec.type === 'string') {
84
+ visit(rec, ancestors)
85
+ ancestors = [...ancestors, rec]
86
+ }
87
+
88
+ for (const key of Object.keys(node)) {
89
+ if (key === 'parent') continue
90
+ const v = rec[key]
91
+ if (v && typeof v === 'object') {
92
+ walkAstWithAncestors(v, ancestors, visit)
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Парсить файл і повертає `program` або null, якщо є синтаксичні помилки чи виняток.
99
+ * @param {string} content вихідний код
100
+ * @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
101
+ * @returns {unknown | null} `result.program` або null, якщо парсинг не вдався
102
+ */
103
+ export function parseProgramOrNull(content, virtualPath) {
104
+ const lang = langFromPath(virtualPath || 'scan.ts')
105
+ let result
106
+ try {
107
+ result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
108
+ } catch {
109
+ return null
110
+ }
111
+ if (result.errors?.length) return null
112
+ return result.program
113
+ }
114
+
115
+ /**
116
+ * Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
117
+ * @param {unknown} node AST node
118
+ * @returns {boolean} true, якщо це CallExpression `*.join(...)`
119
+ */
120
+ export function isJoinCall(node) {
121
+ if (!node || node.type !== 'CallExpression') return false
122
+ const callee = node.callee
123
+ if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
124
+ const prop = callee.property
125
+ return !!prop && prop.type === 'Identifier' && prop.name === 'join'
126
+ }
127
+
128
+ /**
129
+ * Текст quasis у TemplateLiteral (без expressions).
130
+ * @param {unknown} template TemplateLiteral
131
+ * @returns {string} обʼєднаний raw-текст
132
+ */
133
+ export function templateQuasisText(template) {
134
+ if (!template || template.type !== 'TemplateLiteral') return ''
135
+ const quasis = template.quasis
136
+ if (!Array.isArray(quasis) || quasis.length === 0) return ''
137
+ let out = ''
138
+ for (const q of quasis) {
139
+ if (!q || typeof q !== 'object') continue
140
+ const value = q.value
141
+ if (!value || typeof value !== 'object') continue
142
+ if (typeof value.raw === 'string') out += value.raw
143
+ }
144
+ return out
145
+ }
146
+
147
+ /**
148
+ * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
149
+ * @param {unknown} template TemplateLiteral
150
+ * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
151
+ */
152
+ export function isSqlListContextTemplate(template) {
153
+ return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
154
+ }
@@ -15,112 +15,19 @@
15
15
  * Якщо файл не парситься / містить синтаксичні помилки — повертаємо порожній
16
16
  * результат: спочатку треба полагодити синтаксис, потім перезапустити перевірку.
17
17
  */
18
- import { parseSync } from 'oxc-parser'
18
+ import {
19
+ isFunctionNode,
20
+ isJoinCall,
21
+ isSqlListContextTemplate,
22
+ normalizeSnippet,
23
+ offsetToLine,
24
+ parseProgramOrNull,
25
+ walkAstWithAncestors
26
+ } from './ast-scan-utils.mjs'
19
27
 
20
28
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
21
- const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
22
29
  const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
23
30
 
24
- /**
25
- * Мова для Oxc за шляхом файлу (розширення).
26
- * @param {string} filePath віртуальний або реальний шлях до файлу
27
- * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
28
- */
29
- function langFromPath(filePath) {
30
- const lower = filePath.toLowerCase()
31
- if (lower.endsWith('.tsx')) return 'tsx'
32
- if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
33
- if (lower.endsWith('.jsx')) return 'jsx'
34
- return 'js'
35
- }
36
-
37
- /**
38
- * Номер рядка (1-based) за зміщенням у буфері.
39
- * @param {string} content повний текст файлу
40
- * @param {number} offset байтове зміщення початку фрагмента
41
- * @returns {number} номер рядка від 1
42
- */
43
- function offsetToLine(content, offset) {
44
- let line = 1
45
- const n = Math.min(offset, content.length)
46
- for (let i = 0; i < n; i++) {
47
- if (content.codePointAt(i) === 10) line++
48
- }
49
- return line
50
- }
51
-
52
- /**
53
- * Стискає пробіли для повідомлення про порушення.
54
- * @param {string} s фрагмент коду
55
- * @returns {string} скорочений однорядковий рядок
56
- */
57
- function normalizeSnippet(s) {
58
- return s.replaceAll(/\s+/gu, ' ').trim().slice(0, 180)
59
- }
60
-
61
- /**
62
- * Чи є вузол функцією.
63
- * @param {unknown} node AST node
64
- * @returns {boolean} true, якщо це будь-який вузол-функція
65
- */
66
- function isFunctionNode(node) {
67
- return (
68
- !!node &&
69
- typeof node === 'object' &&
70
- typeof node.type === 'string' &&
71
- (node.type === 'FunctionDeclaration' ||
72
- node.type === 'FunctionExpression' ||
73
- node.type === 'ArrowFunctionExpression')
74
- )
75
- }
76
-
77
- /**
78
- * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
79
- * @param {unknown} node поточний вузол
80
- * @param {unknown[]} ancestors масив предків від кореня до parent
81
- * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
82
- * @returns {void}
83
- */
84
- function walkAstWithAncestors(node, ancestors, visit) {
85
- if (!node || typeof node !== 'object') return
86
- if (Array.isArray(node)) {
87
- for (const item of node) walkAstWithAncestors(item, ancestors, visit)
88
- return
89
- }
90
-
91
- const rec = /** @type {Record<string, unknown>} */ (node)
92
- if (typeof rec.type === 'string') {
93
- visit(rec, ancestors)
94
- ancestors = [...ancestors, rec]
95
- }
96
-
97
- for (const key of Object.keys(node)) {
98
- if (key === 'parent') continue
99
- const v = rec[key]
100
- if (v && typeof v === 'object') {
101
- walkAstWithAncestors(v, ancestors, visit)
102
- }
103
- }
104
- }
105
-
106
- /**
107
- * Парсить файл та повертає program або null, якщо є синтаксичні помилки.
108
- * @param {string} content вихідний код
109
- * @param {string} virtualPath шлях для вибору `lang`
110
- * @returns {unknown | null} `result.program` або null
111
- */
112
- function parseProgramOrNull(content, virtualPath) {
113
- const lang = langFromPath(virtualPath || 'scan.ts')
114
- let result
115
- try {
116
- result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
117
- } catch {
118
- return null
119
- }
120
- if (result.errors?.length) return null
121
- return result.program
122
- }
123
-
124
31
  /**
125
32
  * Чи це `new SQL(...)` (Identifier callee з імʼям `SQL`).
126
33
  * @param {unknown} node AST node
@@ -152,47 +59,6 @@ function isUnsafeCallWithInterpolatedTemplate(node) {
152
59
  return Array.isArray(expressions) && expressions.length > 0
153
60
  }
154
61
 
155
- /**
156
- * Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
157
- * @param {unknown} node AST node
158
- * @returns {boolean} true, якщо це CallExpression `*.join(...)`
159
- */
160
- function isJoinCall(node) {
161
- if (!node || node.type !== 'CallExpression') return false
162
- const callee = node.callee
163
- if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
164
- const prop = callee.property
165
- return !!prop && prop.type === 'Identifier' && prop.name === 'join'
166
- }
167
-
168
- /**
169
- * Текст quasis у TemplateLiteral (без expressions).
170
- * @param {unknown} template TemplateLiteral
171
- * @returns {string} обʼєднаний raw-текст
172
- */
173
- function templateQuasisText(template) {
174
- if (!template || template.type !== 'TemplateLiteral') return ''
175
- const quasis = template.quasis
176
- if (!Array.isArray(quasis) || quasis.length === 0) return ''
177
- let out = ''
178
- for (const q of quasis) {
179
- if (!q || typeof q !== 'object') continue
180
- const value = q.value
181
- if (!value || typeof value !== 'object') continue
182
- if (typeof value.raw === 'string') out += value.raw
183
- }
184
- return out
185
- }
186
-
187
- /**
188
- * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
189
- * @param {unknown} template TemplateLiteral
190
- * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
191
- */
192
- function isSqlListContextTemplate(template) {
193
- return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
194
- }
195
-
196
62
  /**
197
63
  * Знаходить `new SQL(...)` всередині функцій (handler на кожен запит замість singleton).
198
64
  * @param {string} content вихідний код
@@ -11,45 +11,11 @@
11
11
  */
12
12
  import { parseSync } from 'oxc-parser'
13
13
 
14
+ import { langFromPath, offsetToLine } from './ast-scan-utils.mjs'
15
+
14
16
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
15
17
  const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
16
18
 
17
- /**
18
- * Мова для Oxc за шляхом файлу (розширення).
19
- * @param {string} filePath віртуальний або реальний шлях до файлу
20
- * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
21
- */
22
- function langFromPath(filePath) {
23
- const lower = filePath.toLowerCase()
24
- if (lower.endsWith('.tsx')) {
25
- return 'tsx'
26
- }
27
- if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) {
28
- return 'ts'
29
- }
30
- if (lower.endsWith('.jsx')) {
31
- return 'jsx'
32
- }
33
- return 'js'
34
- }
35
-
36
- /**
37
- * Номер рядка (1-based) за зміщенням у буфері.
38
- * @param {string} content повний текст файлу
39
- * @param {number} offset байтове зміщення початку фрагмента
40
- * @returns {number} номер рядка від 1
41
- */
42
- function offsetToLine(content, offset) {
43
- let line = 1
44
- const n = Math.min(offset, content.length)
45
- for (let i = 0; i < n; i++) {
46
- if (content.codePointAt(i) === 10) {
47
- line++
48
- }
49
- }
50
- return line
51
- }
52
-
53
19
  /**
54
20
  * Стискає пробіли для повідомлення про порушення.
55
21
  * @param {string} s фрагмент коду
@@ -22,95 +22,20 @@
22
22
  */
23
23
  import { parseSync } from 'oxc-parser'
24
24
 
25
+ import {
26
+ isFunctionNode,
27
+ isJoinCall,
28
+ isSqlListContextTemplate,
29
+ langFromPath,
30
+ normalizeSnippet,
31
+ offsetToLine,
32
+ walkAstWithAncestors
33
+ } from './ast-scan-utils.mjs'
34
+
25
35
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
26
- const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
27
36
  const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
28
37
  const NUMERIC_PARSE_FN_NAMES = new Set(['parseInt', 'parseFloat', 'Number', 'BigInt'])
29
38
 
30
- /**
31
- * Мова для Oxc за шляхом файлу (розширення).
32
- * @param {string} filePath віртуальний або реальний шлях до файлу
33
- * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
34
- */
35
- function langFromPath(filePath) {
36
- const lower = filePath.toLowerCase()
37
- if (lower.endsWith('.tsx')) return 'tsx'
38
- if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
39
- if (lower.endsWith('.jsx')) return 'jsx'
40
- return 'js'
41
- }
42
-
43
- /**
44
- * Номер рядка (1-based) за зміщенням у буфері.
45
- * @param {string} content повний текст файлу
46
- * @param {number} offset байтове зміщення початку фрагмента
47
- * @returns {number} номер рядка від 1
48
- */
49
- function offsetToLine(content, offset) {
50
- let line = 1
51
- const n = Math.min(offset, content.length)
52
- for (let i = 0; i < n; i++) {
53
- if (content.codePointAt(i) === 10) line++
54
- }
55
- return line
56
- }
57
-
58
- /**
59
- * Стискає пробіли для повідомлення про порушення.
60
- * @param {string} s фрагмент коду
61
- * @returns {string} скорочений однорядковий рядок
62
- */
63
- function normalizeSnippet(s) {
64
- return s.replaceAll(/\s+/g, ' ').trim().slice(0, 180)
65
- }
66
-
67
- /**
68
- * Чи є вузол функцією.
69
- * @param {unknown} node AST node
70
- * @returns {boolean} true, якщо це будь-який вузол-функція
71
- */
72
- function isFunctionNode(node) {
73
- return (
74
- !!node &&
75
- typeof node === 'object' &&
76
- typeof node.type === 'string' &&
77
- (node.type === 'FunctionDeclaration' ||
78
- node.type === 'FunctionExpression' ||
79
- node.type === 'ArrowFunctionExpression')
80
- )
81
- }
82
-
83
- /**
84
- * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
85
- * @param {unknown} node поточний вузол
86
- * @param {unknown[]} ancestors масив предків від кореня до parent
87
- * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
88
- * @returns {void}
89
- */
90
- function walkAstWithAncestors(node, ancestors, visit) {
91
- if (!node || typeof node !== 'object') return
92
- if (Array.isArray(node)) {
93
- for (const item of node) walkAstWithAncestors(item, ancestors, visit)
94
- return
95
- }
96
-
97
- const rec = /** @type {Record<string, unknown>} */ (node)
98
- if (typeof rec.type === 'string') {
99
- visit(rec, ancestors)
100
- ancestors = [...ancestors, rec]
101
- }
102
-
103
- for (const key of Object.keys(node)) {
104
- if (key === 'parent') {
105
- continue
106
- }
107
- const v = rec[key]
108
- if (v && typeof v === 'object') {
109
- walkAstWithAncestors(v, ancestors, visit)
110
- }
111
- }
112
- }
113
-
114
39
  /**
115
40
  * Чи це `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)`.
116
41
  * @param {unknown} node AST node
@@ -160,48 +85,6 @@ function isRequestFactoryCall(node) {
160
85
  return !!prop && prop.type === 'Identifier' && prop.name === 'request'
161
86
  }
162
87
 
163
- /**
164
- * Чи це `.join(...)` виклик (часто використовується для динамічних списків в SQL).
165
- * @param {unknown} node AST node
166
- * @returns {boolean} true, якщо це CallExpression `*.join(...)`
167
- */
168
- function isJoinCall(node) {
169
- if (!node || node.type !== 'CallExpression') return false
170
- const callee = node.callee
171
- if (!callee || callee.type !== 'MemberExpression') return false
172
- if (callee.computed) return false
173
- const prop = callee.property
174
- return !!prop && prop.type === 'Identifier' && prop.name === 'join'
175
- }
176
-
177
- /**
178
- * Повертає текст quasis у TemplateLiteral (без expressions).
179
- * @param {unknown} template TemplateLiteral
180
- * @returns {string} обʼєднаний текст
181
- */
182
- function templateQuasisText(template) {
183
- if (!template || template.type !== 'TemplateLiteral') return ''
184
- const quasis = template.quasis
185
- if (!Array.isArray(quasis) || quasis.length === 0) return ''
186
- let out = ''
187
- for (const q of quasis) {
188
- if (!q || typeof q !== 'object') continue
189
- const value = q.value
190
- if (!value || typeof value !== 'object') continue
191
- if (typeof value.raw === 'string') out += value.raw
192
- }
193
- return out
194
- }
195
-
196
- /**
197
- * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
198
- * @param {unknown} template TemplateLiteral
199
- * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
200
- */
201
- function isSqlListContextTemplate(template) {
202
- return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
203
- }
204
-
205
88
  /**
206
89
  * Знаходить створення `ConnectionPool` всередині функцій.
207
90
  * @param {string} content вихідний код
@@ -449,13 +332,7 @@ function isInListExpressionParsed(expr, declarators, seen = new Set()) {
449
332
  const inits = declarators
450
333
  .filter(d => {
451
334
  const id = d.id
452
- return (
453
- !!id &&
454
- typeof id === 'object' &&
455
- id.type === 'Identifier' &&
456
- id.name === expr.name &&
457
- !!d.init
458
- )
335
+ return !!id && typeof id === 'object' && id.type === 'Identifier' && id.name === expr.name && !!d.init
459
336
  })
460
337
  .map(d => d.init)
461
338
  if (inits.length === 0) return false
@@ -541,4 +418,3 @@ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan
541
418
  export function isMssqlScanSourceFile(relativePathPosix) {
542
419
  return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
543
420
  }
544
-
@@ -53,7 +53,7 @@ bun run lint
53
53
 
54
54
  **Чому взагалі кілька `eslint`:** оркестратор (Claude Code, Cursor тощо) може **розпаралелити** роботу: кілька **субагентів** / **паралельних Bash-задач** / **фонових shell**, і кожен **сам** виконує **`/n-lint`** або запускає **`eslint`**. Тоді навантаження **множиться**: не один прогон лінту, а **N прогонів** одночасно.
55
55
 
56
- **Що робити агенту під час виконання цього скілу (обов’язково)**
56
+ ### Що робити агенту під час виконання цього скілу (обов’язково)
57
57
 
58
58
  1. **Один** запуск **`bun run lint`** (або всі кроки **`lint`**, як у **`package.json`**) — у **одному** foreground shell, **без** `run_in_background` / фонових копій тієї ж команди.
59
59
  2. **Не** викликати **паралельні субагенти** (subagent, Task, «розбий на N паралельних завдань») лише заради лінту в одному репозиторії. Лінт не потребує шардінгу: один процес, послідовно.