@aiready/consistency 0.3.3 → 0.3.5

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/dist/index.js CHANGED
@@ -32,12 +32,155 @@ var import_core3 = require("@aiready/core");
32
32
 
33
33
  // src/analyzers/naming.ts
34
34
  var import_core = require("@aiready/core");
35
+ var import_path = require("path");
36
+ var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
37
+ // Full English words (1-3 letters)
38
+ "day",
39
+ "key",
40
+ "net",
41
+ "to",
42
+ "go",
43
+ "for",
44
+ "not",
45
+ "new",
46
+ "old",
47
+ "top",
48
+ "end",
49
+ "run",
50
+ "try",
51
+ "use",
52
+ "get",
53
+ "set",
54
+ "add",
55
+ "put",
56
+ "map",
57
+ "log",
58
+ "row",
59
+ "col",
60
+ "tab",
61
+ "box",
62
+ "div",
63
+ "nav",
64
+ "tag",
65
+ "any",
66
+ "all",
67
+ "one",
68
+ "two",
69
+ "out",
70
+ "off",
71
+ "on",
72
+ "yes",
73
+ "no",
74
+ "now",
75
+ "max",
76
+ "min",
77
+ "sum",
78
+ "avg",
79
+ "ref",
80
+ "src",
81
+ "dst",
82
+ "raw",
83
+ "def",
84
+ "sub",
85
+ "pub",
86
+ "pre",
87
+ "mid",
88
+ "alt",
89
+ "opt",
90
+ "tmp",
91
+ "ext",
92
+ "sep",
93
+ // Additional full words commonly flagged
94
+ "tax",
95
+ "cat",
96
+ "dog",
97
+ "car",
98
+ "bus",
99
+ "web",
100
+ "app",
101
+ "war",
102
+ "law",
103
+ "pay",
104
+ "buy",
105
+ "win",
106
+ "cut",
107
+ "hit",
108
+ "hot",
109
+ "pop",
110
+ "job",
111
+ "age",
112
+ "act",
113
+ "let",
114
+ "lot",
115
+ "bad",
116
+ "big",
117
+ "far",
118
+ "few",
119
+ "own",
120
+ "per",
121
+ "red",
122
+ "low",
123
+ "see",
124
+ "six",
125
+ "ten",
126
+ "way",
127
+ "who",
128
+ "why",
129
+ "yet",
130
+ "via",
131
+ "due",
132
+ "fee",
133
+ "fun",
134
+ "gas",
135
+ "gay",
136
+ "god",
137
+ "gun",
138
+ "guy",
139
+ "ice",
140
+ "ill",
141
+ "kid",
142
+ "mad",
143
+ "man",
144
+ "mix",
145
+ "mom",
146
+ "mrs",
147
+ "nor",
148
+ "odd",
149
+ "oil",
150
+ "pan",
151
+ "pet",
152
+ "pit",
153
+ "pot",
154
+ "pow",
155
+ "pro",
156
+ "raw",
157
+ "rep",
158
+ "rid",
159
+ "sad",
160
+ "sea",
161
+ "sit",
162
+ "sky",
163
+ "son",
164
+ "tea",
165
+ "tie",
166
+ "tip",
167
+ "van",
168
+ "war",
169
+ "win",
170
+ "won"
171
+ ]);
35
172
  var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
36
173
  // Standard identifiers
37
174
  "id",
38
175
  "uid",
39
176
  "gid",
40
177
  "pid",
178
+ // Loop counters and iterators
179
+ "i",
180
+ "j",
181
+ "k",
182
+ "n",
183
+ "m",
41
184
  // Web/Network
42
185
  "url",
43
186
  "uri",
@@ -55,6 +198,9 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
55
198
  "rss",
56
199
  "xhr",
57
200
  "ajax",
201
+ "cors",
202
+ "ws",
203
+ "wss",
58
204
  // Data formats
59
205
  "json",
60
206
  "xml",
@@ -64,12 +210,27 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
64
210
  "css",
65
211
  "svg",
66
212
  "pdf",
213
+ // File types & extensions
214
+ "img",
215
+ "txt",
216
+ "doc",
217
+ "docx",
218
+ "xlsx",
219
+ "ppt",
220
+ "md",
221
+ "rst",
222
+ "jpg",
223
+ "png",
224
+ "gif",
67
225
  // Databases
68
226
  "db",
69
227
  "sql",
70
228
  "orm",
71
229
  "dao",
72
230
  "dto",
231
+ "ddb",
232
+ "rds",
233
+ "nosql",
73
234
  // File system
74
235
  "fs",
75
236
  "dir",
@@ -86,6 +247,8 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
86
247
  "cli",
87
248
  "cmd",
88
249
  "exe",
250
+ "cwd",
251
+ "pwd",
89
252
  // UI/UX
90
253
  "ui",
91
254
  "ux",
@@ -98,6 +261,7 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
98
261
  "ctx",
99
262
  "err",
100
263
  "msg",
264
+ "auth",
101
265
  // Mathematics/Computing
102
266
  "max",
103
267
  "min",
@@ -115,12 +279,17 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
115
279
  "var",
116
280
  "int",
117
281
  "num",
282
+ "idx",
118
283
  // Time
119
284
  "now",
120
285
  "utc",
121
286
  "tz",
122
287
  "ms",
123
288
  "sec",
289
+ "hr",
290
+ "min",
291
+ "yr",
292
+ "mo",
124
293
  // Common patterns
125
294
  "app",
126
295
  "cfg",
@@ -140,47 +309,169 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
140
309
  "post",
141
310
  "sub",
142
311
  "pub",
312
+ // Programming/Framework specific
313
+ "ts",
314
+ "js",
315
+ "jsx",
316
+ "tsx",
317
+ "py",
318
+ "rb",
319
+ "vue",
320
+ "re",
321
+ "fn",
322
+ "fns",
323
+ "mod",
324
+ "opts",
325
+ "dev",
326
+ // Cloud/Infrastructure
327
+ "s3",
328
+ "ec2",
329
+ "sqs",
330
+ "sns",
331
+ "vpc",
332
+ "ami",
333
+ "iam",
334
+ "acl",
335
+ "elb",
336
+ "alb",
337
+ "nlb",
338
+ "aws",
339
+ // Metrics/Performance
340
+ "fcp",
341
+ "lcp",
342
+ "cls",
343
+ "ttfb",
344
+ "tti",
345
+ "fid",
346
+ "fps",
347
+ "qps",
348
+ "rps",
349
+ "tps",
350
+ // Testing & i18n
351
+ "po",
352
+ "e2e",
353
+ "a11y",
354
+ "i18n",
355
+ "l10n",
356
+ // Domain-specific abbreviations (context-aware)
357
+ "sk",
358
+ "fy",
359
+ "faq",
360
+ "og",
361
+ "seo",
362
+ "cta",
363
+ "roi",
364
+ "kpi",
143
365
  // Boolean helpers (these are intentional short names)
144
366
  "is",
145
367
  "has",
146
368
  "can",
147
369
  "did",
148
370
  "was",
149
- "are"
371
+ "are",
372
+ // Date/Time context (when in date contexts)
373
+ "d",
374
+ "t",
375
+ "dt"
150
376
  ]);
151
377
  async function analyzeNaming(files) {
152
378
  const issues = [];
379
+ const rootDir = files.length > 0 ? (0, import_path.dirname)(files[0]) : process.cwd();
380
+ const config = (0, import_core.loadConfig)(rootDir);
381
+ const consistencyConfig = config?.tools?.["consistency"];
382
+ const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
383
+ const customShortWords = new Set(consistencyConfig?.shortWords || []);
384
+ const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
153
385
  for (const file of files) {
154
386
  const content = await (0, import_core.readFileContent)(file);
155
- const fileIssues = analyzeFileNaming(file, content);
387
+ const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
156
388
  issues.push(...fileIssues);
157
389
  }
158
390
  return issues;
159
391
  }
160
- function analyzeFileNaming(file, content) {
392
+ function analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks) {
161
393
  const issues = [];
394
+ const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
162
395
  const lines = content.split("\n");
396
+ const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
397
+ const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
398
+ const getContextWindow = (index, windowSize = 3) => {
399
+ const start = Math.max(0, index - windowSize);
400
+ const end = Math.min(lines.length, index + windowSize + 1);
401
+ return lines.slice(start, end).join("\n");
402
+ };
403
+ const isShortLivedVariable = (varName, declarationIndex) => {
404
+ const searchRange = 5;
405
+ const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
406
+ let usageCount = 0;
407
+ for (let i = declarationIndex; i < endIndex; i++) {
408
+ const regex = new RegExp(`\\b${varName}\\b`, "g");
409
+ const matches = lines[i].match(regex);
410
+ if (matches) {
411
+ usageCount += matches.length;
412
+ }
413
+ }
414
+ return usageCount >= 2 && usageCount <= 3;
415
+ };
163
416
  lines.forEach((line, index) => {
164
417
  const lineNumber = index + 1;
165
- const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
166
- for (const match of singleLetterMatches) {
167
- const letter = match[1].toLowerCase();
168
- const isInLoopContext = line.includes("for") || line.includes(".map") || line.includes(".filter") || line.includes(".forEach") || line.includes(".reduce");
169
- if (!isInLoopContext && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
170
- issues.push({
171
- file,
172
- line: lineNumber,
173
- type: "poor-naming",
174
- identifier: match[1],
175
- severity: "minor",
176
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
177
- });
418
+ const contextWindow = getContextWindow(index);
419
+ if (!disabledChecks.has("single-letter")) {
420
+ const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
421
+ for (const match of singleLetterMatches) {
422
+ const letter = match[1].toLowerCase();
423
+ const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
424
+ /\w+\s*=>\s*/.test(line);
425
+ const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
426
+ const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
427
+ /[a-z]\s*=>/.test(line) || // s => on same line
428
+ // Multi-line arrow function detection: look for pattern in context window
429
+ new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
430
+ new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
431
+ const isShortLived = isShortLivedVariable(letter, index);
432
+ if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
433
+ if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
434
+ continue;
435
+ }
436
+ issues.push({
437
+ file,
438
+ line: lineNumber,
439
+ type: "poor-naming",
440
+ identifier: match[1],
441
+ severity: "minor",
442
+ suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
443
+ });
444
+ }
178
445
  }
179
446
  }
180
- const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
181
- for (const match of abbreviationMatches) {
182
- const abbrev = match[1].toLowerCase();
183
- if (!ACCEPTABLE_ABBREVIATIONS.has(abbrev)) {
447
+ if (!disabledChecks.has("abbreviation")) {
448
+ const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
449
+ for (const match of abbreviationMatches) {
450
+ const abbrev = match[1].toLowerCase();
451
+ if (allShortWords.has(abbrev)) {
452
+ continue;
453
+ }
454
+ if (allAbbreviations.has(abbrev)) {
455
+ continue;
456
+ }
457
+ const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
458
+ new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
459
+ // Multi-line arrow function: check context window
460
+ new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
461
+ new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
462
+ if (isArrowFunctionParam) {
463
+ continue;
464
+ }
465
+ if (abbrev.length <= 2) {
466
+ const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
467
+ if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
468
+ continue;
469
+ }
470
+ const isUserContext = /user|auth|account/i.test(line);
471
+ if (isUserContext && abbrev === "u") {
472
+ continue;
473
+ }
474
+ }
184
475
  issues.push({
185
476
  file,
186
477
  line: lineNumber,
@@ -191,7 +482,7 @@ function analyzeFileNaming(file, content) {
191
482
  });
192
483
  }
193
484
  }
194
- if (file.match(/\.(ts|tsx|js|jsx)$/)) {
485
+ if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
195
486
  const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
196
487
  const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
197
488
  if (snakeCaseVars) {
@@ -205,36 +496,51 @@ function analyzeFileNaming(file, content) {
205
496
  });
206
497
  }
207
498
  }
208
- const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
209
- for (const match of booleanMatches) {
210
- const name = match[1];
211
- if (!name.match(/^(is|has|should|can|will|did)/i)) {
212
- issues.push({
213
- file,
214
- line: lineNumber,
215
- type: "unclear",
216
- identifier: name,
217
- severity: "info",
218
- suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
219
- });
499
+ if (!disabledChecks.has("unclear")) {
500
+ const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
501
+ for (const match of booleanMatches) {
502
+ const name = match[1];
503
+ if (!name.match(/^(is|has|should|can|will|did)/i)) {
504
+ issues.push({
505
+ file,
506
+ line: lineNumber,
507
+ type: "unclear",
508
+ identifier: name,
509
+ severity: "info",
510
+ suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
511
+ });
512
+ }
220
513
  }
221
514
  }
222
- const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
223
- for (const match of functionMatches) {
224
- const name = match[1];
225
- const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator)$/);
226
- const isEventHandler = name.match(/^on[A-Z]/);
227
- const isDescriptiveLong = name.length > 20;
228
- const hasActionVerb = name.match(/^(get|set|is|has|can|should|create|update|delete|fetch|load|save|process|handle|validate|check|find|search|filter|map|reduce|make|do|run|start|stop|build|parse|format|render|calculate|compute|generate|transform|convert|normalize|sanitize|encode|decode|compress|extract|merge|split|join|sort|compare|test|verify|ensure|apply|execute|invoke|call|emit|dispatch|trigger|listen|subscribe|unsubscribe|add|remove|clear|reset|toggle|enable|disable|open|close|connect|disconnect|send|receive|read|write|import|export|register|unregister|mount|unmount)/);
229
- if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong) {
230
- issues.push({
231
- file,
232
- line: lineNumber,
233
- type: "unclear",
234
- identifier: name,
235
- severity: "info",
236
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
237
- });
515
+ if (!disabledChecks.has("unclear")) {
516
+ const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
517
+ for (const match of functionMatches) {
518
+ const name = match[1];
519
+ const isKeyword = ["for", "if", "else", "while", "do", "switch", "case", "break", "continue", "return", "throw", "try", "catch", "finally", "with", "yield", "await"].includes(name);
520
+ if (isKeyword) {
521
+ continue;
522
+ }
523
+ const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(name);
524
+ if (isEntryPoint) {
525
+ continue;
526
+ }
527
+ const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator)$/);
528
+ const isEventHandler = name.match(/^on[A-Z]/);
529
+ const isDescriptiveLong = name.length > 15;
530
+ const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) || name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props)$/);
531
+ const capitalCount = (name.match(/[A-Z]/g) || []).length;
532
+ const isCompoundWord = capitalCount >= 3;
533
+ const hasActionVerb = name.match(/^(get|set|is|has|can|should|create|update|delete|fetch|load|save|process|handle|validate|check|find|search|filter|map|reduce|make|do|run|start|stop|build|parse|format|render|calculate|compute|generate|transform|convert|normalize|sanitize|encode|decode|compress|extract|merge|split|join|sort|compare|test|verify|ensure|apply|execute|invoke|call|emit|dispatch|trigger|listen|subscribe|unsubscribe|add|remove|clear|reset|toggle|enable|disable|open|close|connect|disconnect|send|receive|read|write|import|export|register|unregister|mount|unmount)/);
534
+ if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord) {
535
+ issues.push({
536
+ file,
537
+ line: lineNumber,
538
+ type: "unclear",
539
+ identifier: name,
540
+ severity: "info",
541
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
542
+ });
543
+ }
238
544
  }
239
545
  }
240
546
  });
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  analyzeNaming,
4
4
  analyzePatterns,
5
5
  detectNamingConventions
6
- } from "./chunk-LUAREV6A.mjs";
6
+ } from "./chunk-2BTBNG6X.mjs";
7
7
  export {
8
8
  analyzeConsistency,
9
9
  analyzeNaming,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/consistency",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Detects consistency issues in naming, patterns, and architecture that confuse AI models",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "chalk": "^5.3.0",
43
43
  "commander": "^12.1.0",
44
- "@aiready/core": "0.7.0"
44
+ "@aiready/core": "0.7.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^22.10.5",
@@ -49,12 +49,25 @@ const result = x + y;
49
49
  const acceptableAbbreviations = [
50
50
  'env', 'req', 'res', 'ctx', 'err', 'api', 'url', 'id',
51
51
  'max', 'min', 'now', 'utm', 'has', 'is', 'can',
52
- 'db', 'fs', 'os', 'ui', 'tmp', 'src', 'dst'
52
+ 'db', 'fs', 'os', 'ui', 'tmp', 'src', 'dst',
53
+ // New additions from Phase 1
54
+ 'img', 'txt', 'doc', 'md', 'ts', 'js', 'ddb', 's3',
55
+ 'fcp', 'lcp', 'fps', 'po', 'dto', 'e2e', 'a11y', 'i18n'
53
56
  ];
54
57
  // These abbreviations should not trigger warnings
55
58
  expect(acceptableAbbreviations.length).toBeGreaterThan(0);
56
59
  });
57
60
 
61
+ it('should NOT flag common short English words', () => {
62
+ // Full words, not abbreviations - should be accepted
63
+ const commonWords = [
64
+ 'day', 'key', 'net', 'to', 'go', 'for', 'not', 'new', 'old',
65
+ 'top', 'end', 'run', 'try', 'use', 'get', 'set', 'add', 'put'
66
+ ];
67
+ // These are full words and should not be flagged as abbreviations
68
+ expect(commonWords.length).toBeGreaterThan(0);
69
+ });
70
+
58
71
  it('should detect snake_case in TypeScript files', () => {
59
72
  const testCode = `
60
73
  const user_name = 'John';
@@ -82,6 +95,33 @@ const api = new ApiClient();
82
95
  // Should not flag these as issues
83
96
  expect(true).toBe(true);
84
97
  });
98
+
99
+ it('should NOT flag multi-line arrow function parameters (Phase 3)', () => {
100
+ // Multi-line arrow functions should not trigger single-letter warnings
101
+ const multiLineArrowCode = `
102
+ items.map(
103
+ s => s.value
104
+ )
105
+
106
+ items.filter(
107
+ item =>
108
+ item.valid
109
+ )
110
+ `;
111
+ // 's' and 'item' should not be flagged as poor naming
112
+ expect(true).toBe(true);
113
+ });
114
+
115
+ it('should NOT flag short-lived comparison variables (Phase 3)', () => {
116
+ // Variables used only within 3-5 lines for comparisons
117
+ const shortLivedCode = `
118
+ const a = obj1;
119
+ const b = obj2;
120
+ return compare(a, b);
121
+ `;
122
+ // 'a' and 'b' should not be flagged as they're short-lived
123
+ expect(true).toBe(true);
124
+ });
85
125
  });
86
126
 
87
127
  describe('analyzePatterns', () => {