@pattern-stack/frontend-patterns 0.2.0-alpha.0 → 0.2.0-alpha.3

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 (56) hide show
  1. package/cli/commands/generate-hooks.ts +13 -4
  2. package/cli/src/codegen/openapi/__tests__/naming-utils.test.js +367 -0
  3. package/cli/src/codegen/openapi/client-generator.js +87 -19
  4. package/cli/src/codegen/openapi/confidence-scorer.js +93 -0
  5. package/cli/src/codegen/openapi/hook-config.js +48 -0
  6. package/cli/src/codegen/openapi/hook-generator.js +100 -62
  7. package/cli/src/codegen/openapi/naming-constants.js +98 -0
  8. package/cli/src/codegen/openapi/naming-utils.js +149 -0
  9. package/dist/atoms/components/core/Badge/Badge.d.ts +1 -1
  10. package/dist/atoms/components/data/DataTable/DataTable.d.ts.map +1 -1
  11. package/dist/atoms/components/data/DataTable/DataTable.types.d.ts +15 -3
  12. package/dist/atoms/components/data/DataTable/DataTable.types.d.ts.map +1 -1
  13. package/dist/atoms/hooks/index.d.ts +2 -0
  14. package/dist/atoms/hooks/index.d.ts.map +1 -1
  15. package/dist/atoms/hooks/useFieldMetadata.d.ts +18 -0
  16. package/dist/atoms/hooks/useFieldMetadata.d.ts.map +1 -0
  17. package/dist/atoms/hooks/useResponsiveTable.d.ts +103 -0
  18. package/dist/atoms/hooks/useResponsiveTable.d.ts.map +1 -0
  19. package/dist/atoms/primitives/sheet.d.ts +23 -0
  20. package/dist/atoms/primitives/sheet.d.ts.map +1 -0
  21. package/dist/atoms/services/auth-service.d.ts.map +1 -1
  22. package/dist/atoms/types/auth.d.ts +51 -0
  23. package/dist/atoms/types/auth.d.ts.map +1 -1
  24. package/dist/atoms/types/index.d.ts +1 -0
  25. package/dist/atoms/types/index.d.ts.map +1 -1
  26. package/dist/atoms/types/ui-config.d.ts +25 -8
  27. package/dist/atoms/types/ui-config.d.ts.map +1 -1
  28. package/dist/atoms/types/ui-metadata.d.ts +112 -0
  29. package/dist/atoms/types/ui-metadata.d.ts.map +1 -0
  30. package/dist/atoms/utils/ui-mapping.d.ts +9 -3
  31. package/dist/atoms/utils/ui-mapping.d.ts.map +1 -1
  32. package/dist/features/auth/hooks/useAuth.d.ts.map +1 -1
  33. package/dist/frontend-patterns.css +82 -0
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.es.js +1030 -248
  37. package/dist/index.es.js.map +1 -1
  38. package/dist/index.js +1031 -248
  39. package/dist/index.js.map +1 -1
  40. package/dist/molecules/layout/ListToolbar/ListToolbar.d.ts +37 -0
  41. package/dist/molecules/layout/ListToolbar/ListToolbar.d.ts.map +1 -0
  42. package/dist/molecules/layout/ListToolbar/index.d.ts +2 -0
  43. package/dist/molecules/layout/ListToolbar/index.d.ts.map +1 -0
  44. package/dist/molecules/layout/PageTitle/PageTitle.d.ts +17 -0
  45. package/dist/molecules/layout/PageTitle/PageTitle.d.ts.map +1 -0
  46. package/dist/molecules/layout/PageTitle/index.d.ts +2 -0
  47. package/dist/molecules/layout/PageTitle/index.d.ts.map +1 -0
  48. package/dist/molecules/layout/index.d.ts +2 -0
  49. package/dist/molecules/layout/index.d.ts.map +1 -1
  50. package/dist/templates/ListPageTemplate.d.ts +21 -0
  51. package/dist/templates/ListPageTemplate.d.ts.map +1 -0
  52. package/dist/templates/admin/AdminCRUDTemplate.d.ts.map +1 -1
  53. package/dist/templates/factory.d.ts.map +1 -1
  54. package/dist/templates/index.d.ts +1 -0
  55. package/dist/templates/index.d.ts.map +1 -1
  56. package/package.json +4 -3
@@ -8,11 +8,15 @@
8
8
 
9
9
  import { promises as fs } from 'fs'
10
10
  import { join, dirname } from 'path'
11
+ import { fileURLToPath } from 'url'
11
12
  import { loadOpenAPISpec, parseOpenAPI } from '../../src/codegen/openapi/parser.js'
12
13
  import { generateTypes } from '../../src/codegen/openapi/type-generator.js'
13
14
  import { generateAPIClient } from '../../src/codegen/openapi/client-generator.js'
14
15
  import { generateHooks } from '../../src/codegen/openapi/hook-generator.js'
15
16
 
17
+ const __filename = fileURLToPath(import.meta.url)
18
+ const __dirname = dirname(__filename)
19
+
16
20
  interface GenerateHooksOptions {
17
21
  output: string
18
22
  prefix?: string
@@ -121,6 +125,11 @@ export async function generateHooksCommand(source: string, options: GenerateHook
121
125
  await writeFile(join(options.output, 'hooks', 'keys.ts'), hooks.keys)
122
126
  await writeFile(join(options.output, 'hooks', 'types.ts'), hooks.types)
123
127
  await writeFile(join(options.output, 'hooks', 'index.ts'), hooks.index)
128
+
129
+ // Copy bulk-types.ts to output directory (referenced by hooks/types.ts)
130
+ const bulkTypesPath = join(__dirname, '../../src/codegen/openapi/bulk-types.ts')
131
+ const bulkTypesContent = await fs.readFile(bulkTypesPath, 'utf8')
132
+ await writeFile(join(options.output, 'bulk-types.ts'), bulkTypesContent)
124
133
 
125
134
  // Write confidence report if available
126
135
  if (hooks.report) {
@@ -154,10 +163,10 @@ export { queryKeys } from './hooks'`
154
163
 
155
164
  await writeFile(join(options.output, 'index.ts'), mainIndex)
156
165
 
157
- // Generate usage example
166
+ // Generate usage example (as .tsx since it contains JSX)
158
167
  const usageExample = generateUsageExample(spec.info.title, options)
159
- await writeFile(join(options.output, 'example.ts'), usageExample)
160
-
168
+ await writeFile(join(options.output, 'example.tsx'), usageExample)
169
+
161
170
  console.log()
162
171
  console.log('✅ Generation complete!')
163
172
  console.log(`📦 Generated files in: ${options.output}`)
@@ -165,7 +174,7 @@ export { queryKeys } from './hooks'`
165
174
  console.log('🚀 Quick start:')
166
175
  console.log(`import { createAPIClient, useGetUsers } from './${options.output}'`)
167
176
  console.log()
168
- console.log('💡 See example.ts for detailed usage instructions')
177
+ console.log('💡 See example.tsx for detailed usage instructions')
169
178
 
170
179
  } catch (error) {
171
180
  console.error('❌ Generation failed:')
@@ -0,0 +1,367 @@
1
+ import { singularize, pluralize, isPlural } from '../naming-utils.js';
2
+
3
+ describe('singularize()', () => {
4
+ describe('regular plurals', () => {
5
+ it('should convert cats to cat', () => {
6
+ expect(singularize('cats')).toBe('cat');
7
+ });
8
+
9
+ it('should convert dogs to dog', () => {
10
+ expect(singularize('dogs')).toBe('dog');
11
+ });
12
+
13
+ it('should convert users to user', () => {
14
+ expect(singularize('users')).toBe('user');
15
+ });
16
+
17
+ it('should convert accounts to account', () => {
18
+ expect(singularize('accounts')).toBe('account');
19
+ });
20
+ });
21
+
22
+ describe('-ies plurals', () => {
23
+ it('should convert activities to activity', () => {
24
+ expect(singularize('activities')).toBe('activity');
25
+ });
26
+
27
+ it('should convert categories to category', () => {
28
+ expect(singularize('categories')).toBe('category');
29
+ });
30
+
31
+ it('should convert companies to company', () => {
32
+ expect(singularize('companies')).toBe('company');
33
+ });
34
+
35
+ it('should convert opportunities to opportunity', () => {
36
+ expect(singularize('opportunities')).toBe('opportunity');
37
+ });
38
+ });
39
+
40
+ describe('-es plurals', () => {
41
+ it('should convert boxes to box', () => {
42
+ expect(singularize('boxes')).toBe('box');
43
+ });
44
+
45
+ it('should convert watches to watch', () => {
46
+ expect(singularize('watches')).toBe('watch');
47
+ });
48
+
49
+ it('should convert dishes to dish', () => {
50
+ expect(singularize('dishes')).toBe('dish');
51
+ });
52
+
53
+ it('should convert benches to bench', () => {
54
+ expect(singularize('benches')).toBe('bench');
55
+ });
56
+ });
57
+
58
+ describe('-ses plurals (tricky cases)', () => {
59
+ it('should convert addresses to address', () => {
60
+ expect(singularize('addresses')).toBe('address');
61
+ });
62
+
63
+ it('should convert businesses to business', () => {
64
+ expect(singularize('businesses')).toBe('business');
65
+ });
66
+
67
+ it('should convert processes to process', () => {
68
+ expect(singularize('processes')).toBe('process');
69
+ });
70
+ });
71
+
72
+ describe('irregular plurals', () => {
73
+ it('should convert people to person', () => {
74
+ expect(singularize('people')).toBe('person');
75
+ });
76
+
77
+ it('should convert children to child', () => {
78
+ expect(singularize('children')).toBe('child');
79
+ });
80
+
81
+ it('should convert men to man', () => {
82
+ expect(singularize('men')).toBe('man');
83
+ });
84
+
85
+ it('should convert women to woman', () => {
86
+ expect(singularize('women')).toBe('woman');
87
+ });
88
+ });
89
+
90
+ describe('already singular', () => {
91
+ it('should keep data as data', () => {
92
+ expect(singularize('data')).toBe('data');
93
+ });
94
+
95
+ it('should keep metadata as metadata', () => {
96
+ expect(singularize('metadata')).toBe('metadata');
97
+ });
98
+
99
+ it('should keep user as user', () => {
100
+ expect(singularize('user')).toBe('user');
101
+ });
102
+
103
+ it('should keep account as account', () => {
104
+ expect(singularize('account')).toBe('account');
105
+ });
106
+ });
107
+
108
+ describe('edge cases', () => {
109
+ it('should keep status as status', () => {
110
+ expect(singularize('status')).toBe('status');
111
+ });
112
+
113
+ it('should keep class as class', () => {
114
+ expect(singularize('class')).toBe('class');
115
+ });
116
+
117
+ it('should keep bus as bus', () => {
118
+ expect(singularize('bus')).toBe('bus');
119
+ });
120
+
121
+ it('should convert pass to pass (not pas)', () => {
122
+ expect(singularize('pass')).toBe('pass');
123
+ });
124
+
125
+ it('should handle empty string', () => {
126
+ expect(singularize('')).toBe('');
127
+ });
128
+
129
+ it('should handle single character', () => {
130
+ expect(singularize('a')).toBe('a');
131
+ });
132
+ });
133
+ });
134
+
135
+ describe('pluralize()', () => {
136
+ describe('regular plurals', () => {
137
+ it('should convert cat to cats', () => {
138
+ expect(pluralize('cat')).toBe('cats');
139
+ });
140
+
141
+ it('should convert dog to dogs', () => {
142
+ expect(pluralize('dog')).toBe('dogs');
143
+ });
144
+
145
+ it('should convert user to users', () => {
146
+ expect(pluralize('user')).toBe('users');
147
+ });
148
+
149
+ it('should convert account to accounts', () => {
150
+ expect(pluralize('account')).toBe('accounts');
151
+ });
152
+ });
153
+
154
+ describe('-y endings (consonant + y)', () => {
155
+ it('should convert activity to activities', () => {
156
+ expect(pluralize('activity')).toBe('activities');
157
+ });
158
+
159
+ it('should convert category to categories', () => {
160
+ expect(pluralize('category')).toBe('categories');
161
+ });
162
+
163
+ it('should convert company to companies', () => {
164
+ expect(pluralize('company')).toBe('companies');
165
+ });
166
+
167
+ it('should convert opportunity to opportunities', () => {
168
+ expect(pluralize('opportunity')).toBe('opportunities');
169
+ });
170
+ });
171
+
172
+ describe('vowel + y endings', () => {
173
+ it('should convert day to days', () => {
174
+ expect(pluralize('day')).toBe('days');
175
+ });
176
+
177
+ it('should convert key to keys', () => {
178
+ expect(pluralize('key')).toBe('keys');
179
+ });
180
+
181
+ it('should convert boy to boys', () => {
182
+ expect(pluralize('boy')).toBe('boys');
183
+ });
184
+
185
+ it('should convert array to arrays', () => {
186
+ expect(pluralize('array')).toBe('arrays');
187
+ });
188
+ });
189
+
190
+ describe('-x, -ch, -sh, -s, -ss endings', () => {
191
+ it('should convert box to boxes', () => {
192
+ expect(pluralize('box')).toBe('boxes');
193
+ });
194
+
195
+ it('should convert watch to watches', () => {
196
+ expect(pluralize('watch')).toBe('watches');
197
+ });
198
+
199
+ it('should convert dish to dishes', () => {
200
+ expect(pluralize('dish')).toBe('dishes');
201
+ });
202
+
203
+ it('should convert bench to benches', () => {
204
+ expect(pluralize('bench')).toBe('benches');
205
+ });
206
+
207
+ it('should convert address to addresses', () => {
208
+ expect(pluralize('address')).toBe('addresses');
209
+ });
210
+
211
+ it('should convert business to businesses', () => {
212
+ expect(pluralize('business')).toBe('businesses');
213
+ });
214
+
215
+ it('should convert class to classes', () => {
216
+ expect(pluralize('class')).toBe('classes');
217
+ });
218
+ });
219
+
220
+ describe('irregular plurals', () => {
221
+ it('should convert person to people', () => {
222
+ expect(pluralize('person')).toBe('people');
223
+ });
224
+
225
+ it('should convert child to children', () => {
226
+ expect(pluralize('child')).toBe('children');
227
+ });
228
+
229
+ it('should convert man to men', () => {
230
+ expect(pluralize('man')).toBe('men');
231
+ });
232
+
233
+ it('should convert woman to women', () => {
234
+ expect(pluralize('woman')).toBe('women');
235
+ });
236
+ });
237
+
238
+ describe('already plural (no double-pluralize)', () => {
239
+ it('should keep accounts as accounts', () => {
240
+ expect(pluralize('accounts')).toBe('accounts');
241
+ });
242
+
243
+ it('should keep users as users', () => {
244
+ expect(pluralize('users')).toBe('users');
245
+ });
246
+
247
+ it('should keep activities as activities', () => {
248
+ expect(pluralize('activities')).toBe('activities');
249
+ });
250
+
251
+ it('should keep boxes as boxes', () => {
252
+ expect(pluralize('boxes')).toBe('boxes');
253
+ });
254
+ });
255
+
256
+ describe('edge cases', () => {
257
+ it('should handle empty string', () => {
258
+ expect(pluralize('')).toBe('');
259
+ });
260
+
261
+ it('should handle single character', () => {
262
+ expect(pluralize('a')).toBe('as');
263
+ });
264
+
265
+ it('should keep status as status (special case)', () => {
266
+ expect(pluralize('status')).toBe('status');
267
+ });
268
+ });
269
+ });
270
+
271
+ describe('isPlural()', () => {
272
+ describe('should return true for plural words', () => {
273
+ it('should identify cats as plural', () => {
274
+ expect(isPlural('cats')).toBe(true);
275
+ });
276
+
277
+ it('should identify activities as plural', () => {
278
+ expect(isPlural('activities')).toBe(true);
279
+ });
280
+
281
+ it('should identify boxes as plural', () => {
282
+ expect(isPlural('boxes')).toBe(true);
283
+ });
284
+
285
+ it('should identify watches as plural', () => {
286
+ expect(isPlural('watches')).toBe(true);
287
+ });
288
+
289
+ it('should identify addresses as plural', () => {
290
+ expect(isPlural('addresses')).toBe(true);
291
+ });
292
+
293
+ it('should identify people as plural', () => {
294
+ expect(isPlural('people')).toBe(true);
295
+ });
296
+
297
+ it('should identify children as plural', () => {
298
+ expect(isPlural('children')).toBe(true);
299
+ });
300
+
301
+ it('should identify users as plural', () => {
302
+ expect(isPlural('users')).toBe(true);
303
+ });
304
+
305
+ it('should identify accounts as plural', () => {
306
+ expect(isPlural('accounts')).toBe(true);
307
+ });
308
+ });
309
+
310
+ describe('should return false for singular words', () => {
311
+ it('should identify cat as singular', () => {
312
+ expect(isPlural('cat')).toBe(false);
313
+ });
314
+
315
+ it('should identify activity as singular', () => {
316
+ expect(isPlural('activity')).toBe(false);
317
+ });
318
+
319
+ it('should identify box as singular', () => {
320
+ expect(isPlural('box')).toBe(false);
321
+ });
322
+
323
+ it('should identify user as singular', () => {
324
+ expect(isPlural('user')).toBe(false);
325
+ });
326
+
327
+ it('should identify account as singular', () => {
328
+ expect(isPlural('account')).toBe(false);
329
+ });
330
+ });
331
+
332
+ describe('should return false for edge cases', () => {
333
+ it('should identify status as singular', () => {
334
+ expect(isPlural('status')).toBe(false);
335
+ });
336
+
337
+ it('should identify class as singular', () => {
338
+ expect(isPlural('class')).toBe(false);
339
+ });
340
+
341
+ it('should identify bus as singular', () => {
342
+ expect(isPlural('bus')).toBe(false);
343
+ });
344
+
345
+ it('should identify process as singular', () => {
346
+ expect(isPlural('process')).toBe(false);
347
+ });
348
+
349
+ it('should identify data as singular', () => {
350
+ expect(isPlural('data')).toBe(false);
351
+ });
352
+
353
+ it('should identify metadata as singular', () => {
354
+ expect(isPlural('metadata')).toBe(false);
355
+ });
356
+ });
357
+
358
+ describe('should handle empty/minimal input', () => {
359
+ it('should return false for empty string', () => {
360
+ expect(isPlural('')).toBe(false);
361
+ });
362
+
363
+ it('should return false for single character', () => {
364
+ expect(isPlural('a')).toBe(false);
365
+ });
366
+ });
367
+ });
@@ -6,6 +6,9 @@
6
6
  *
7
7
  * Part of FRO-12: API Client Generator
8
8
  */
9
+ import { singularize, pluralize, isPlural } from './naming-utils.js';
10
+ import { AUTH_ACTIONS, HEALTH_ENDPOINTS, USER_PROFILE_ENDPOINTS, SINGLETON_RESOURCES, COLLECTION_ACTIONS } from './naming-constants.js';
11
+
9
12
  export class APIClientGenerator {
10
13
  options;
11
14
  constructor(options = {}) {
@@ -39,7 +42,7 @@ export class APIClientGenerator {
39
42
  generateAxiosClient() {
40
43
  return `/**
41
44
  * Axios API Client
42
- *
45
+ *
43
46
  * Auto-generated API client with interceptors and error handling
44
47
  */
45
48
 
@@ -146,7 +149,7 @@ export class APIClient {
146
149
  generateFetchClient() {
147
150
  return `/**
148
151
  * Fetch API Client
149
- *
152
+ *
150
153
  * Auto-generated API client using native fetch with error handling
151
154
  */
152
155
 
@@ -166,7 +169,7 @@ export class APIClient {
166
169
  ): Promise<T> {
167
170
  try {
168
171
  const fullUrl = this.buildUrl(url, options.params)
169
-
172
+
170
173
  const fetchOptions: RequestInit = {
171
174
  method,
172
175
  headers: {
@@ -205,7 +208,7 @@ export class APIClient {
205
208
 
206
209
  private buildUrl(path: string, params?: Record<string, any>): string {
207
210
  const url = new URL(path, this.config.baseUrl)
208
-
211
+
209
212
  if (params) {
210
213
  Object.entries(params).forEach(([key, value]) => {
211
214
  if (value !== undefined && value !== null) {
@@ -325,7 +328,7 @@ export class APIClient {
325
328
  private shouldRetry(error: AxiosError): boolean {
326
329
  const retryCount = (error.config as any)?._retryCount || 0
327
330
  const maxRetries = this.config.retries || ${this.options.retries}
328
-
331
+
329
332
  return (
330
333
  retryCount < maxRetries &&
331
334
  (!error.response || error.response.status >= 500) &&
@@ -335,11 +338,11 @@ export class APIClient {
335
338
 
336
339
  private async retryRequest(config: any): Promise<any> {
337
340
  config._retryCount = (config._retryCount || 0) + 1
338
-
341
+
339
342
  // Exponential backoff
340
343
  const delay = Math.pow(2, config._retryCount) * 1000
341
344
  await new Promise(resolve => setTimeout(resolve, delay))
342
-
345
+
343
346
  return this.client.request(config)
344
347
  }`;
345
348
  }
@@ -500,7 +503,7 @@ export class APIClient {
500
503
  const authTypes = this.generateAuthTypes();
501
504
  return `/**
502
505
  * API Client Types
503
- *
506
+ *
504
507
  * Configuration and utility types for the generated API client
505
508
  */
506
509
 
@@ -556,7 +559,7 @@ export interface APIError {
556
559
  generateConfiguration() {
557
560
  return `/**
558
561
  * API Client Configuration
559
- *
562
+ *
560
563
  * Default configuration and factory functions
561
564
  */
562
565
 
@@ -580,7 +583,7 @@ export const defaultConfig: Partial<APIClientConfig> = {
580
583
  generateIndexFile() {
581
584
  return `/**
582
585
  * Generated API Client
583
- *
586
+ *
584
587
  * Auto-generated from OpenAPI specification
585
588
  */
586
589
 
@@ -593,20 +596,85 @@ export { createAPIClient, defaultConfig } from './config'`;
593
596
  generateFileHeader(title) {
594
597
  return `/**
595
598
  * ${title}
596
- *
599
+ *
597
600
  * Auto-generated from OpenAPI specification
598
601
  * Do not edit manually
599
602
  */`;
600
603
  }
601
604
  getMethodName(endpoint) {
602
- if (endpoint.operationId) {
603
- return this.camelCase(endpoint.operationId);
604
- }
605
- const pathParts = endpoint.path
606
- .split('/')
607
- .filter(part => part && !part.startsWith('{'))
608
- .map(part => this.capitalize(part));
609
- return this.camelCase(`${endpoint.method}_${pathParts.join('_')}`);
605
+ // Use semantic naming instead of raw operationId
606
+ return this.generateSemanticMethodName(endpoint);
607
+ }
608
+ generateSemanticMethodName(endpoint) {
609
+ // Extract resource from path
610
+ const pathSegments = endpoint.path.split('/').filter(s => s && !s.startsWith('{') && !['api', 'v1', 'v2'].includes(s));
611
+ const urlSegments = endpoint.path.split('/');
612
+ const hasPathParam = endpoint.path.includes('{');
613
+ const lastSegment = pathSegments[pathSegments.length - 1]?.replace(/-/g, '_') || 'resource';
614
+ const prevSegment = pathSegments[pathSegments.length - 2]?.replace(/-/g, '_');
615
+
616
+ // Auth endpoints - use action name directly
617
+ if (AUTH_ACTIONS.includes(lastSegment.toLowerCase())) {
618
+ return this.camelCase(lastSegment);
619
+ }
620
+
621
+ // Health/status singletons
622
+ if (HEALTH_ENDPOINTS.includes(lastSegment.toLowerCase())) {
623
+ return this.camelCase(lastSegment);
624
+ }
625
+
626
+ // User profile singletons
627
+ if (USER_PROFILE_ENDPOINTS.includes(lastSegment.toLowerCase())) {
628
+ return this.camelCase(`get_current_user`);
629
+ }
630
+
631
+ // Metadata endpoints - include parent resource to avoid collision
632
+ if (lastSegment.toLowerCase() === 'metadata' && prevSegment) {
633
+ return hasPathParam
634
+ ? this.camelCase(`get_${singularize(prevSegment)}_metadata`)
635
+ : this.camelCase(`get_${prevSegment}_metadata`);
636
+ }
637
+
638
+ // Collection actions (e.g., /activities/search)
639
+ if (COLLECTION_ACTIONS.includes(lastSegment.toLowerCase()) && prevSegment) {
640
+ return this.camelCase(`${lastSegment}_${prevSegment}`);
641
+ }
642
+
643
+ // Determine resource name - for nested paths, include context
644
+ let resource = lastSegment;
645
+ if (pathSegments.length > 2 && hasPathParam) {
646
+ // e.g., /accounts/{id}/stages/allowed → get_account_allowed_stages
647
+ const parentResource = pathSegments.find((s, i) => {
648
+ const nextSeg = urlSegments[urlSegments.findIndex(u => u === s) + 1];
649
+ return nextSeg?.startsWith('{');
650
+ });
651
+ if (parentResource && parentResource !== lastSegment) {
652
+ resource = `${singularize(parentResource)}_${lastSegment}`;
653
+ }
654
+ }
655
+
656
+ // Generate name based on HTTP method
657
+ switch (endpoint.method) {
658
+ case 'get':
659
+ return hasPathParam
660
+ ? this.camelCase(`get_${singularize(resource)}`)
661
+ : this.camelCase(`list_${resource}`);
662
+ case 'post':
663
+ // Check for custom actions like /accounts/{id}/stage
664
+ const lastUrl = urlSegments[urlSegments.length - 1];
665
+ const prevUrl = urlSegments[urlSegments.length - 2];
666
+ if (!lastUrl.startsWith('{') && prevUrl?.startsWith('{')) {
667
+ return this.camelCase(`${lastUrl}_${singularize(prevSegment || resource)}`);
668
+ }
669
+ return this.camelCase(`create_${singularize(resource)}`);
670
+ case 'put':
671
+ case 'patch':
672
+ return this.camelCase(`update_${singularize(resource)}`);
673
+ case 'delete':
674
+ return this.camelCase(`delete_${singularize(resource)}`);
675
+ default:
676
+ return this.camelCase(`${endpoint.method}_${resource}`);
677
+ }
610
678
  }
611
679
  generatePathWithParams(endpoint) {
612
680
  return endpoint.path.replace(/{([^}]+)}/g, '${$1}');
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Confidence Scorer (Stub)
3
+ *
4
+ * Scores generated hook names for quality/confidence.
5
+ * This is a minimal stub that returns high confidence for all names.
6
+ */
7
+
8
+ export class ConfidenceScorer {
9
+ constructor() {
10
+ this.scoringRules = [];
11
+ }
12
+
13
+ /**
14
+ * Score a generated hook name
15
+ * @returns {{ hookName: string, operationId: string, confidence: string, score: number, suggestions: string[] }}
16
+ */
17
+ scoreHookName(hookName, operationId, path, method) {
18
+ // Simple heuristic: shorter names are better
19
+ const lengthScore = Math.max(0, 100 - hookName.length);
20
+
21
+ // Check for common anti-patterns
22
+ const hasApiV1 = hookName.toLowerCase().includes('apiv1');
23
+ const hasUnderscore = hookName.includes('_');
24
+ const isVeryLong = hookName.length > 40;
25
+
26
+ let score = lengthScore;
27
+ if (hasApiV1) score -= 30;
28
+ if (hasUnderscore) score -= 10;
29
+ if (isVeryLong) score -= 20;
30
+
31
+ // Normalize to 0-100
32
+ score = Math.max(0, Math.min(100, score));
33
+
34
+ // Determine confidence level
35
+ let confidence;
36
+ if (score >= 70) confidence = 'high';
37
+ else if (score >= 40) confidence = 'medium';
38
+ else confidence = 'low';
39
+
40
+ return {
41
+ hookName,
42
+ operationId,
43
+ path,
44
+ method,
45
+ confidence,
46
+ score,
47
+ suggestions: confidence === 'low' ? [this.suggestBetterName(path, method)] : []
48
+ };
49
+ }
50
+
51
+ suggestBetterName(path, method) {
52
+ // Extract resource from path
53
+ const segments = path.split('/').filter(s => s && !s.startsWith('{') && !['api', 'v1'].includes(s));
54
+ const resource = segments[segments.length - 1]?.replace(/-/g, '') || 'resource';
55
+
56
+ // Capitalize first letter
57
+ const capitalizedResource = resource.charAt(0).toUpperCase() + resource.slice(1);
58
+
59
+ switch (method) {
60
+ case 'get':
61
+ return path.includes('{') ? `useGet${capitalizedResource}` : `use${capitalizedResource}`;
62
+ case 'post':
63
+ return `useCreate${capitalizedResource}`;
64
+ case 'put':
65
+ case 'patch':
66
+ return `useUpdate${capitalizedResource}`;
67
+ case 'delete':
68
+ return `useDelete${capitalizedResource}`;
69
+ default:
70
+ return `use${capitalizedResource}`;
71
+ }
72
+ }
73
+
74
+ generateReport(scoredNames) {
75
+ const high = scoredNames.filter(s => s.confidence === 'high').length;
76
+ const medium = scoredNames.filter(s => s.confidence === 'medium').length;
77
+ const low = scoredNames.filter(s => s.confidence === 'low').length;
78
+
79
+ return `
80
+ Hook Name Quality Report
81
+ ========================
82
+ Total hooks: ${scoredNames.length}
83
+ High confidence: ${high} (${Math.round(high/scoredNames.length*100)}%)
84
+ Medium confidence: ${medium} (${Math.round(medium/scoredNames.length*100)}%)
85
+ Low confidence: ${low} (${Math.round(low/scoredNames.length*100)}%)
86
+
87
+ ${low > 0 ? 'Low confidence hooks that may need review:\n' +
88
+ scoredNames.filter(s => s.confidence === 'low')
89
+ .map(s => ` - ${s.hookName} → suggested: ${s.suggestions[0]}`)
90
+ .join('\n') : 'All hooks have acceptable names!'}
91
+ `;
92
+ }
93
+ }