@schemyx/mcp 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +76 -2
  2. package/dist/backend-client.d.ts +1 -17
  3. package/dist/backend-client.js +15 -91
  4. package/dist/backend-client.js.map +1 -1
  5. package/dist/client/backend-client.d.ts +17 -0
  6. package/dist/client/backend-client.js +97 -0
  7. package/dist/client/backend-client.js.map +1 -0
  8. package/dist/client/local-theme-source.d.ts +19 -0
  9. package/dist/client/local-theme-source.js +737 -0
  10. package/dist/client/local-theme-source.js.map +1 -0
  11. package/dist/client/mcp-client.d.ts +15 -0
  12. package/dist/client/mcp-client.js +46 -0
  13. package/dist/client/mcp-client.js.map +1 -0
  14. package/dist/codebase-scanner/backend.d.ts +12 -0
  15. package/dist/codebase-scanner/backend.js +814 -0
  16. package/dist/codebase-scanner/backend.js.map +1 -0
  17. package/dist/codebase-scanner/bundle.d.ts +81 -0
  18. package/dist/codebase-scanner/bundle.js +2177 -0
  19. package/dist/codebase-scanner/bundle.js.map +1 -0
  20. package/dist/codebase-scanner/constants.d.ts +26 -0
  21. package/dist/codebase-scanner/constants.js +230 -0
  22. package/dist/codebase-scanner/constants.js.map +1 -0
  23. package/dist/codebase-scanner/extractors.d.ts +187 -0
  24. package/dist/codebase-scanner/extractors.js +2600 -0
  25. package/dist/codebase-scanner/extractors.js.map +1 -0
  26. package/dist/codebase-scanner/files.d.ts +16 -0
  27. package/dist/codebase-scanner/files.js +233 -0
  28. package/dist/codebase-scanner/files.js.map +1 -0
  29. package/dist/codebase-scanner/index.d.ts +217 -0
  30. package/dist/codebase-scanner/index.js +387 -0
  31. package/dist/codebase-scanner/index.js.map +1 -0
  32. package/dist/codebase-scanner/recipes.d.ts +74 -0
  33. package/dist/codebase-scanner/recipes.js +585 -0
  34. package/dist/codebase-scanner/recipes.js.map +1 -0
  35. package/dist/codebase-scanner/storage.d.ts +19 -0
  36. package/dist/codebase-scanner/storage.js +103 -0
  37. package/dist/codebase-scanner/storage.js.map +1 -0
  38. package/dist/codebase-scanner/types.d.ts +522 -0
  39. package/dist/codebase-scanner/types.js +3 -0
  40. package/dist/codebase-scanner/types.js.map +1 -0
  41. package/dist/codebase-scanner/utils.d.ts +37 -0
  42. package/dist/codebase-scanner/utils.js +259 -0
  43. package/dist/codebase-scanner/utils.js.map +1 -0
  44. package/dist/codebase-scanner.d.ts +1 -0
  45. package/dist/codebase-scanner.js +18 -0
  46. package/dist/codebase-scanner.js.map +1 -0
  47. package/dist/config.d.ts +1 -2
  48. package/dist/config.js +15 -37
  49. package/dist/config.js.map +1 -1
  50. package/dist/local-theme-source.d.ts +1 -0
  51. package/dist/local-theme-source.js +18 -0
  52. package/dist/local-theme-source.js.map +1 -0
  53. package/dist/main.js +3 -3
  54. package/dist/main.js.map +1 -1
  55. package/dist/mcp-client.d.ts +1 -0
  56. package/dist/mcp-client.js +18 -0
  57. package/dist/mcp-client.js.map +1 -0
  58. package/dist/prompts.d.ts +1 -7
  59. package/dist/prompts.js +15 -52
  60. package/dist/prompts.js.map +1 -1
  61. package/dist/server/index.d.ts +4 -0
  62. package/dist/server/index.js +163 -0
  63. package/dist/server/index.js.map +1 -0
  64. package/dist/server/prompts.d.ts +7 -0
  65. package/dist/server/prompts.js +55 -0
  66. package/dist/server/prompts.js.map +1 -0
  67. package/dist/server/tool-definitions.d.ts +22 -0
  68. package/dist/server/tool-definitions.js +531 -0
  69. package/dist/server/tool-definitions.js.map +1 -0
  70. package/dist/server.d.ts +3 -3
  71. package/dist/server.js +33 -0
  72. package/dist/server.js.map +1 -1
  73. package/dist/shared/config.d.ts +2 -0
  74. package/dist/shared/config.js +54 -0
  75. package/dist/shared/config.js.map +1 -0
  76. package/dist/shared/text.d.ts +14 -0
  77. package/dist/shared/text.js +33 -0
  78. package/dist/shared/text.js.map +1 -0
  79. package/dist/shared/types.d.ts +118 -0
  80. package/dist/shared/types.js +3 -0
  81. package/dist/shared/types.js.map +1 -0
  82. package/dist/shared/uri.d.ts +6 -0
  83. package/dist/shared/uri.js +24 -0
  84. package/dist/shared/uri.js.map +1 -0
  85. package/dist/style-recipes.d.ts +1 -0
  86. package/dist/style-recipes.js +18 -0
  87. package/dist/style-recipes.js.map +1 -0
  88. package/dist/text.d.ts +1 -14
  89. package/dist/text.js +15 -30
  90. package/dist/text.js.map +1 -1
  91. package/dist/theme/style-recipes.d.ts +26 -0
  92. package/dist/theme/style-recipes.js +129 -0
  93. package/dist/theme/style-recipes.js.map +1 -0
  94. package/dist/tool-definitions.d.ts +1 -11
  95. package/dist/tool-definitions.js +15 -127
  96. package/dist/tool-definitions.js.map +1 -1
  97. package/dist/types.d.ts +1 -106
  98. package/dist/types.js +15 -0
  99. package/dist/types.js.map +1 -1
  100. package/dist/uri.d.ts +1 -6
  101. package/dist/uri.js +15 -21
  102. package/dist/uri.js.map +1 -1
  103. package/package.json +5 -2
@@ -0,0 +1,2177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBundle = createBundle;
4
+ exports.addEntry = addEntry;
5
+ exports.hasStyleSurface = hasStyleSurface;
6
+ exports.isBackendRouteFile = isBackendRouteFile;
7
+ exports.backendFileContext = backendFileContext;
8
+ exports.findEndpointContract = findEndpointContract;
9
+ exports.createBackendEndpointFlow = createBackendEndpointFlow;
10
+ exports.backendCallDependencies = backendCallDependencies;
11
+ exports.addBackendCallEdges = addBackendCallEdges;
12
+ exports.backendCallKey = backendCallKey;
13
+ exports.uniqueSideEffectsForRecipe = uniqueSideEffectsForRecipe;
14
+ exports.sourceInfo = sourceInfo;
15
+ exports.toGraphNode = toGraphNode;
16
+ exports.toGraphEdge = toGraphEdge;
17
+ exports.addRenderEdges = addRenderEdges;
18
+ exports.addUiPatternRules = addUiPatternRules;
19
+ exports.addUiCompositionRules = addUiCompositionRules;
20
+ exports.addConnectedPatternRules = addConnectedPatternRules;
21
+ exports.buildConnectedPatternClusters = buildConnectedPatternClusters;
22
+ exports.createFrontendPatternInstances = createFrontendPatternInstances;
23
+ exports.createBackendPatternInstances = createBackendPatternInstances;
24
+ exports.createDataPatternInstances = createDataPatternInstances;
25
+ exports.isConnectedUiPatternRoot = isConnectedUiPatternRoot;
26
+ exports.normalizeConnectedChildren = normalizeConnectedChildren;
27
+ exports.withoutConnectedChildSignature = withoutConnectedChildSignature;
28
+ exports.connectedChildKind = connectedChildKind;
29
+ exports.summarizeConnectedChildren = summarizeConnectedChildren;
30
+ exports.compactConnectedPatternInstance = compactConnectedPatternInstance;
31
+ exports.scoreConnectedPatternGroup = scoreConnectedPatternGroup;
32
+ exports.connectedPatternConfidence = connectedPatternConfidence;
33
+ exports.createConnectedPatternScores = createConnectedPatternScores;
34
+ exports.createConnectedPatternDecision = createConnectedPatternDecision;
35
+ exports.createConceptPatternScores = createConceptPatternScores;
36
+ exports.createConceptPatternDecision = createConceptPatternDecision;
37
+ exports.connectedPatternVariants = connectedPatternVariants;
38
+ exports.connectedPatternChildren = connectedPatternChildren;
39
+ exports.derivePatternOwners = derivePatternOwners;
40
+ exports.readablePatternConcept = readablePatternConcept;
41
+ exports.roundPatternScore = roundPatternScore;
42
+ exports.frontendPatternWeight = frontendPatternWeight;
43
+ exports.backendPatternWeight = backendPatternWeight;
44
+ exports.hasBackendPatternSurface = hasBackendPatternSurface;
45
+ exports.backendPatternConcept = backendPatternConcept;
46
+ exports.summarizeModelFieldShape = summarizeModelFieldShape;
47
+ exports.createConnectedPatternGuidance = createConnectedPatternGuidance;
48
+ exports.connectedPatternSlug = connectedPatternSlug;
49
+ exports.classSignature = classSignature;
50
+ exports.normalizeLayoutRole = normalizeLayoutRole;
51
+ exports.normalizeTagForSignature = normalizeTagForSignature;
52
+ exports.normalizeChildTagForSignature = normalizeChildTagForSignature;
53
+ exports.labelSignature = labelSignature;
54
+ exports.addImportEdges = addImportEdges;
55
+ exports.addApiCallEdges = addApiCallEdges;
56
+ exports.addStyleUsageEdges = addStyleUsageEdges;
57
+ exports.addModelUsageEdges = addModelUsageEdges;
58
+ exports.addConceptClusters = addConceptClusters;
59
+ exports.applyUsedBy = applyUsedBy;
60
+ exports.detectProjectFacts = detectProjectFacts;
61
+ exports.detectFrameworkForFile = detectFrameworkForFile;
62
+ const backend_1 = require("./backend");
63
+ const constants_1 = require("./constants");
64
+ const extractors_1 = require("./extractors");
65
+ const utils_1 = require("./utils");
66
+ function createBundle(rootPath, projectType, files) {
67
+ const entries = [];
68
+ const usedKeys = new Set();
69
+ const nodes = [];
70
+ const edges = [];
71
+ const reviewItems = [];
72
+ for (const file of files) {
73
+ const fileSource = sourceInfo('core.files', 'high', [`path:${file.relPath}`], file);
74
+ addEntry(entries, usedKeys, {
75
+ key: file.key,
76
+ group: 'file',
77
+ summary: file.summary,
78
+ tags: file.tags,
79
+ dependencies: file.dependencies.map((dependency) => (0, utils_1.toRegistryKey)('dep', dependency)),
80
+ usedBy: [],
81
+ confidence: 'high',
82
+ status: 'inferred',
83
+ path: file.relPath,
84
+ value: {
85
+ path: file.relPath,
86
+ kind: file.kind,
87
+ layer: file.layer,
88
+ language: file.language,
89
+ symbols: file.symbols,
90
+ functions: file.functions,
91
+ classes: file.classes,
92
+ types: file.types,
93
+ constants: file.constants,
94
+ exports: file.exports,
95
+ imports: file.imports,
96
+ classReferences: file.classReferences.slice(0, 80),
97
+ cssIds: file.cssIds.slice(0, 80),
98
+ cssVariableValues: file.cssVariableValues.slice(0, 160),
99
+ cssRules: file.cssRules.slice(0, 120),
100
+ classExpressions: file.classExpressions.slice(0, 160),
101
+ tailwindUtilities: file.tailwindUtilities.slice(0, 80),
102
+ componentStyleDefinitions: file.componentStyleDefinitions.slice(0, 40),
103
+ themeTokens: file.themeTokens.slice(0, 200),
104
+ inlineStyles: file.inlineStyles.slice(0, 80),
105
+ apiCalls: file.apiCalls,
106
+ envVars: file.envVars,
107
+ tests: file.tests,
108
+ docsHeadings: file.docsHeadings,
109
+ configKeys: file.configKeys,
110
+ templateIncludes: file.templateIncludes,
111
+ authHints: file.authHints,
112
+ middleware: file.middleware,
113
+ jobs: file.jobs,
114
+ events: file.events,
115
+ uiElements: file.uiElements.slice(0, 40),
116
+ ...backendFileContext(file),
117
+ size: file.size,
118
+ sourceHash: file.sourceHash,
119
+ source: fileSource,
120
+ },
121
+ });
122
+ nodes.push(toGraphNode(file.key, file.kind, file.relPath, file.relPath, file, 'core.files'));
123
+ for (const dependency of file.dependencies) {
124
+ const dependencyKey = (0, utils_1.toRegistryKey)('dep', dependency);
125
+ addEntry(entries, usedKeys, {
126
+ key: dependencyKey,
127
+ group: 'dependency',
128
+ summary: `Package dependency: ${dependency}.`,
129
+ tags: ['dependency', 'package'],
130
+ dependencies: [],
131
+ usedBy: [],
132
+ confidence: 'high',
133
+ status: 'inferred',
134
+ value: {
135
+ packageName: dependency,
136
+ declaredIn: file.relPath,
137
+ kind: 'dependency',
138
+ source: fileSource,
139
+ },
140
+ });
141
+ nodes.push(toGraphNode(dependencyKey, 'package', dependency, file.relPath, file, 'core.dependencies'));
142
+ edges.push(toGraphEdge(file.key, dependencyKey, 'depends_on', 'core.dependencies', file));
143
+ }
144
+ for (const component of file.components) {
145
+ const componentKey = (0, utils_1.toRegistryKey)('component', component);
146
+ const styleKey = (0, utils_1.toRegistryKey)('style', file.relPath.replace(/\.[^.]+$/, ''));
147
+ const compositionRuleKey = (0, utils_1.toRegistryKey)('rule', `ui.composition.${file.relPath.replace(/\.[^.]+$/, '')}`);
148
+ const dependencies = (0, utils_1.unique)([
149
+ file.key,
150
+ ...(hasStyleSurface(file) ? [styleKey] : []),
151
+ ...(file.uiElements.length ? [compositionRuleKey] : []),
152
+ ]);
153
+ addEntry(entries, usedKeys, {
154
+ key: componentKey,
155
+ group: 'component',
156
+ summary: `${component} component in ${file.relPath}.`,
157
+ tags: ['component', file.layer, file.language],
158
+ dependencies,
159
+ usedBy: [],
160
+ confidence: 'high',
161
+ status: 'inferred',
162
+ path: file.relPath,
163
+ value: {
164
+ name: component,
165
+ path: file.relPath,
166
+ exports: file.exports,
167
+ imports: file.imports,
168
+ props: file.componentProps[component] ?? [],
169
+ classReferences: file.classReferences.slice(0, 40),
170
+ tailwindUtilities: file.tailwindUtilities.slice(0, 40),
171
+ classExpressions: file.classExpressions.slice(0, 80),
172
+ componentStyleDefinitions: file.componentStyleDefinitions.slice(0, 20),
173
+ themeTokens: file.themeTokens.slice(0, 120),
174
+ uiElements: file.uiElements.slice(0, 20),
175
+ apiCalls: file.apiCalls,
176
+ authHints: file.authHints,
177
+ kind: 'component',
178
+ sourceHash: file.sourceHash,
179
+ source: sourceInfo('react', 'high', [`component:${component}`, `path:${file.relPath}`], file),
180
+ },
181
+ });
182
+ nodes.push(toGraphNode(componentKey, 'component', component, file.relPath, file, 'react', {
183
+ react: { export: component },
184
+ }));
185
+ edges.push(toGraphEdge(file.key, componentKey, 'exports', 'typescript', file));
186
+ }
187
+ for (const hook of file.hooks) {
188
+ const hookKey = (0, utils_1.toRegistryKey)('hook', hook);
189
+ addEntry(entries, usedKeys, {
190
+ key: hookKey,
191
+ group: 'hook',
192
+ summary: `${hook} hook in ${file.relPath}.`,
193
+ tags: ['hook', file.layer, file.language],
194
+ dependencies: [file.key],
195
+ usedBy: [],
196
+ confidence: 'high',
197
+ status: 'inferred',
198
+ path: file.relPath,
199
+ value: {
200
+ name: hook,
201
+ path: file.relPath,
202
+ imports: file.imports,
203
+ kind: 'hook',
204
+ sourceHash: file.sourceHash,
205
+ source: sourceInfo('react', 'high', [`hook:${hook}`], file),
206
+ },
207
+ });
208
+ nodes.push(toGraphNode(hookKey, 'hook', hook, file.relPath, file, 'react'));
209
+ edges.push(toGraphEdge(file.key, hookKey, 'exports', 'typescript', file));
210
+ }
211
+ for (const route of file.routes) {
212
+ const routeKey = (0, utils_1.toRegistryKey)('route', route);
213
+ const routeLayer = isBackendRouteFile(file) ? 'backend' : 'frontend';
214
+ const styleKey = (0, utils_1.toRegistryKey)('style', file.relPath.replace(/\.[^.]+$/, ''));
215
+ const compositionRuleKey = (0, utils_1.toRegistryKey)('rule', `ui.composition.${file.relPath.replace(/\.[^.]+$/, '')}`);
216
+ const dependencies = (0, utils_1.unique)([
217
+ file.key,
218
+ ...(hasStyleSurface(file) ? [styleKey] : []),
219
+ ...(file.uiElements.length ? [compositionRuleKey] : []),
220
+ ]);
221
+ addEntry(entries, usedKeys, {
222
+ key: routeKey,
223
+ group: 'route',
224
+ summary: `${routeLayer === 'backend' ? 'Backend' : 'Frontend'} route ${route} from ${file.relPath}.`,
225
+ tags: ['route', routeLayer, file.language],
226
+ dependencies,
227
+ usedBy: [],
228
+ confidence: 'high',
229
+ status: 'inferred',
230
+ path: file.relPath,
231
+ value: {
232
+ name: route,
233
+ route,
234
+ path: file.relPath,
235
+ kind: file.kind,
236
+ uiElements: file.uiElements.slice(0, 40),
237
+ classReferences: file.classReferences.slice(0, 80),
238
+ tailwindUtilities: file.tailwindUtilities.slice(0, 80),
239
+ classExpressions: file.classExpressions.slice(0, 80),
240
+ componentStyleDefinitions: file.componentStyleDefinitions.slice(0, 20),
241
+ apiCalls: file.apiCalls,
242
+ authHints: file.authHints,
243
+ templateIncludes: file.templateIncludes,
244
+ sourceHash: file.sourceHash,
245
+ source: sourceInfo((0, extractors_1.routeExtractorForFile)(file), (0, extractors_1.routeConfidenceForFile)(file), [`route:${route}`, `path:${file.relPath}`], file),
246
+ },
247
+ });
248
+ nodes.push(toGraphNode(routeKey, 'route', route, file.relPath, file, (0, extractors_1.routeExtractorForFile)(file), {
249
+ route: { segment: route },
250
+ }));
251
+ edges.push(toGraphEdge(file.key, routeKey, 'contains', (0, extractors_1.routeExtractorForFile)(file), file, (0, extractors_1.routeConfidenceForFile)(file)));
252
+ }
253
+ for (const handler of file.apiHandlers) {
254
+ const handlerKey = (0, utils_1.toRegistryKey)('api', handler);
255
+ const endpointContract = findEndpointContract(file, handler);
256
+ const backendDependencies = endpointContract
257
+ ? backendCallDependencies(endpointContract.serviceCalls)
258
+ : [];
259
+ addEntry(entries, usedKeys, {
260
+ key: handlerKey,
261
+ group: 'api',
262
+ summary: `API handler ${handler} from ${file.relPath}.`,
263
+ tags: (0, utils_1.unique)([
264
+ 'api',
265
+ 'backend',
266
+ file.layer,
267
+ file.language,
268
+ ...(endpointContract?.risk ?? []),
269
+ ...(endpointContract?.auth.required ? ['auth-protected'] : []),
270
+ ...(endpointContract?.auth.public ? ['public'] : []),
271
+ ]),
272
+ dependencies: (0, utils_1.unique)([file.key, ...backendDependencies]),
273
+ usedBy: [],
274
+ confidence: 'high',
275
+ status: 'inferred',
276
+ path: file.relPath,
277
+ value: {
278
+ name: handler,
279
+ handler,
280
+ path: file.relPath,
281
+ imports: file.imports,
282
+ envVars: file.envVars,
283
+ authHints: file.authHints,
284
+ modelReferences: (0, utils_1.matchingNames)(file.models, file.apiHandlers.join(' ')),
285
+ contract: endpointContract,
286
+ requestContract: endpointContract?.request,
287
+ responseContract: endpointContract?.response,
288
+ authPolicy: endpointContract?.auth,
289
+ validation: endpointContract?.validation,
290
+ serviceCalls: endpointContract?.serviceCalls ?? [],
291
+ repositoryCalls: endpointContract?.repositoryCalls ?? [],
292
+ sideEffects: endpointContract?.sideEffects ?? [],
293
+ errors: endpointContract?.errors ?? [],
294
+ logging: endpointContract?.logging ?? [],
295
+ risk: endpointContract?.risk ?? [],
296
+ flow: endpointContract
297
+ ? createBackendEndpointFlow(file, handlerKey, endpointContract)
298
+ : undefined,
299
+ kind: 'api',
300
+ sourceHash: file.sourceHash,
301
+ source: sourceInfo((0, extractors_1.apiExtractorForFile)(file), 'high', [`handler:${handler}`], file),
302
+ },
303
+ });
304
+ nodes.push(toGraphNode(handlerKey, 'api', handler, file.relPath, file, file.kind.includes('next') ? 'nextjs' : 'api.routes'));
305
+ edges.push(toGraphEdge(file.key, handlerKey, 'contains', 'api.routes', file));
306
+ addBackendCallEdges(handlerKey, endpointContract?.serviceCalls ?? [], file, edges);
307
+ }
308
+ for (const controller of file.controllers) {
309
+ const controllerKey = (0, utils_1.toRegistryKey)('controller', controller);
310
+ const controllerEndpointContracts = file.backendEndpointContracts.filter((contract) => contract.controller === controller);
311
+ addEntry(entries, usedKeys, {
312
+ key: controllerKey,
313
+ group: 'controller',
314
+ summary: `${controller} backend controller in ${file.relPath}.`,
315
+ tags: (0, utils_1.unique)([
316
+ 'controller',
317
+ 'backend',
318
+ file.language,
319
+ ...controllerEndpointContracts.flatMap((contract) => contract.risk),
320
+ ]),
321
+ dependencies: (0, utils_1.unique)([
322
+ file.key,
323
+ ...controllerEndpointContracts.flatMap((contract) => backendCallDependencies(contract.serviceCalls)),
324
+ ]),
325
+ usedBy: [],
326
+ confidence: 'high',
327
+ status: 'inferred',
328
+ path: file.relPath,
329
+ value: {
330
+ name: controller,
331
+ path: file.relPath,
332
+ imports: file.imports,
333
+ apiHandlers: file.apiHandlers,
334
+ endpointContracts: controllerEndpointContracts,
335
+ operations: file.backendOperations.filter((operation) => operation.owner === controller && operation.ownerKind === 'controller'),
336
+ services: file.services,
337
+ authHints: file.authHints,
338
+ sideEffects: uniqueSideEffectsForRecipe(controllerEndpointContracts.flatMap((contract) => contract.sideEffects)),
339
+ errors: controllerEndpointContracts.flatMap((contract) => contract.errors),
340
+ logging: controllerEndpointContracts.flatMap((contract) => contract.logging),
341
+ kind: 'controller',
342
+ sourceHash: file.sourceHash,
343
+ source: sourceInfo((0, extractors_1.controllerExtractorForFile)(file), 'high', [`controller:${controller}`], file),
344
+ },
345
+ });
346
+ nodes.push(toGraphNode(controllerKey, 'controller', controller, file.relPath, file, (0, extractors_1.controllerExtractorForFile)(file)));
347
+ edges.push(toGraphEdge(file.key, controllerKey, 'exports', (0, extractors_1.controllerExtractorForFile)(file), file));
348
+ }
349
+ for (const service of file.services) {
350
+ const serviceKey = (0, utils_1.toRegistryKey)('service', service);
351
+ const serviceOperations = file.backendOperations.filter((operation) => operation.owner === service && operation.ownerKind === 'service');
352
+ addEntry(entries, usedKeys, {
353
+ key: serviceKey,
354
+ group: 'service',
355
+ summary: `${service} backend service in ${file.relPath}.`,
356
+ tags: (0, utils_1.unique)([
357
+ 'service',
358
+ 'backend',
359
+ file.language,
360
+ ...serviceOperations.flatMap((operation) => operation.risk),
361
+ ]),
362
+ dependencies: (0, utils_1.unique)([
363
+ file.key,
364
+ ...backendCallDependencies(serviceOperations.flatMap((operation) => operation.serviceCalls)),
365
+ ]),
366
+ usedBy: [],
367
+ confidence: 'high',
368
+ status: 'inferred',
369
+ path: file.relPath,
370
+ value: {
371
+ name: service,
372
+ path: file.relPath,
373
+ imports: file.imports,
374
+ envVars: file.envVars,
375
+ apiCalls: file.apiCalls,
376
+ operations: serviceOperations,
377
+ operationNames: serviceOperations.map((operation) => operation.name),
378
+ modelReferences: file.models,
379
+ events: file.events,
380
+ sideEffects: uniqueSideEffectsForRecipe(file.backendSideEffects),
381
+ errors: file.backendErrors,
382
+ logging: file.backendLogging,
383
+ risk: (0, utils_1.unique)(serviceOperations.flatMap((operation) => operation.risk)),
384
+ kind: 'service',
385
+ sourceHash: file.sourceHash,
386
+ source: sourceInfo((0, extractors_1.serviceExtractorForFile)(file), 'high', [`service:${service}`], file),
387
+ },
388
+ });
389
+ nodes.push(toGraphNode(serviceKey, 'service', service, file.relPath, file, (0, extractors_1.serviceExtractorForFile)(file)));
390
+ edges.push(toGraphEdge(file.key, serviceKey, 'exports', (0, extractors_1.serviceExtractorForFile)(file), file));
391
+ for (const operation of serviceOperations) {
392
+ addBackendCallEdges(serviceKey, operation.serviceCalls, file, edges);
393
+ }
394
+ }
395
+ for (const moduleName of file.modules) {
396
+ const moduleKey = (0, utils_1.toRegistryKey)('module', moduleName);
397
+ addEntry(entries, usedKeys, {
398
+ key: moduleKey,
399
+ group: 'module',
400
+ summary: `${moduleName} backend module in ${file.relPath}.`,
401
+ tags: ['module', 'backend', file.language],
402
+ dependencies: [file.key],
403
+ usedBy: [],
404
+ confidence: 'high',
405
+ status: 'inferred',
406
+ path: file.relPath,
407
+ value: {
408
+ name: moduleName,
409
+ path: file.relPath,
410
+ imports: file.imports,
411
+ middleware: file.middleware,
412
+ kind: 'module',
413
+ sourceHash: file.sourceHash,
414
+ source: sourceInfo((0, extractors_1.moduleExtractorForFile)(file), 'high', [`module:${moduleName}`], file),
415
+ },
416
+ });
417
+ nodes.push(toGraphNode(moduleKey, 'module', moduleName, file.relPath, file, (0, extractors_1.moduleExtractorForFile)(file)));
418
+ edges.push(toGraphEdge(file.key, moduleKey, 'exports', (0, extractors_1.moduleExtractorForFile)(file), file));
419
+ }
420
+ for (const model of file.models) {
421
+ const modelKey = (0, utils_1.toRegistryKey)('model', model);
422
+ addEntry(entries, usedKeys, {
423
+ key: modelKey,
424
+ group: 'model',
425
+ summary: `${model} data model in ${file.relPath}.`,
426
+ tags: ['model', 'schema'],
427
+ dependencies: [file.key],
428
+ usedBy: [],
429
+ confidence: 'high',
430
+ status: 'inferred',
431
+ path: file.relPath,
432
+ value: {
433
+ name: model,
434
+ path: file.relPath,
435
+ fields: file.modelFields[model] ?? [],
436
+ kind: 'model',
437
+ sourceHash: file.sourceHash,
438
+ source: sourceInfo((0, extractors_1.modelExtractorForFile)(file), 'high', [`model:${model}`], file),
439
+ },
440
+ });
441
+ nodes.push(toGraphNode(modelKey, 'model', model, file.relPath, file, (0, extractors_1.modelExtractorForFile)(file)));
442
+ edges.push(toGraphEdge(file.key, modelKey, 'contains', (0, extractors_1.modelExtractorForFile)(file), file));
443
+ }
444
+ if (hasStyleSurface(file)) {
445
+ const styleKey = (0, utils_1.toRegistryKey)('style', file.relPath.replace(/\.[^.]+$/, ''));
446
+ addEntry(entries, usedKeys, {
447
+ key: styleKey,
448
+ group: 'style',
449
+ summary: `Style surface in ${file.relPath}.`,
450
+ tags: ['style', file.language],
451
+ dependencies: [file.key],
452
+ usedBy: [],
453
+ confidence: 'high',
454
+ status: 'inferred',
455
+ path: file.relPath,
456
+ value: {
457
+ path: file.relPath,
458
+ kind: 'style',
459
+ cssVariables: file.cssVariables,
460
+ cssVariableValues: file.cssVariableValues.slice(0, 200),
461
+ cssRules: file.cssRules.slice(0, 160),
462
+ cssClasses: file.cssClasses,
463
+ classReferences: file.classReferences.slice(0, 120),
464
+ classExpressions: file.classExpressions.slice(0, 160),
465
+ cssIds: file.cssIds,
466
+ cssVariableReferences: file.cssVariableReferences,
467
+ tailwindUtilities: file.tailwindUtilities.slice(0, 120),
468
+ componentStyleDefinitions: file.componentStyleDefinitions.slice(0, 60),
469
+ themeTokens: file.themeTokens.slice(0, 240),
470
+ inlineStyles: file.inlineStyles.slice(0, 100),
471
+ uiElements: file.uiElements.slice(0, 80),
472
+ sourceHash: file.sourceHash,
473
+ source: sourceInfo((0, extractors_1.styleExtractorForFile)(file), (0, extractors_1.styleConfidenceForFile)(file), ['selectors, classes, tokens, or template UI elements'], file),
474
+ },
475
+ });
476
+ nodes.push(toGraphNode(styleKey, 'style', file.relPath, file.relPath, file, (0, extractors_1.styleExtractorForFile)(file)));
477
+ edges.push(toGraphEdge(file.key, styleKey, 'contains', (0, extractors_1.styleExtractorForFile)(file), file, (0, extractors_1.styleConfidenceForFile)(file)));
478
+ }
479
+ for (const styleDefinition of file.componentStyleDefinitions) {
480
+ const styleDefinitionKey = (0, utils_1.toRegistryKey)('style', `${file.relPath.replace(/\.[^.]+$/, '')}.${styleDefinition.name}`);
481
+ addEntry(entries, usedKeys, {
482
+ key: styleDefinitionKey,
483
+ group: 'style',
484
+ summary: `${styleDefinition.name} component style definition in ${file.relPath}.`,
485
+ tags: (0, utils_1.unique)([
486
+ 'style',
487
+ 'component-style',
488
+ styleDefinition.kind,
489
+ styleDefinition.name,
490
+ ...Object.keys(styleDefinition.variants),
491
+ ...Object.values(styleDefinition.variants).flatMap((variant) => Object.keys(variant)),
492
+ ...styleDefinition.classes.slice(0, 24),
493
+ ]),
494
+ dependencies: [file.key],
495
+ usedBy: [],
496
+ confidence: 'high',
497
+ status: 'inferred',
498
+ path: file.relPath,
499
+ value: {
500
+ path: file.relPath,
501
+ kind: 'component-style-definition',
502
+ name: styleDefinition.name,
503
+ definitionKind: styleDefinition.kind,
504
+ line: styleDefinition.line,
505
+ baseClasses: styleDefinition.baseClasses,
506
+ variants: styleDefinition.variants,
507
+ defaultVariants: styleDefinition.defaultVariants,
508
+ classes: styleDefinition.classes,
509
+ sourceSnippet: styleDefinition.source,
510
+ sourceHash: file.sourceHash,
511
+ source: sourceInfo('component-style-definitions', 'high', [`${styleDefinition.kind}:${styleDefinition.name}`, `line:${styleDefinition.line}`], file),
512
+ },
513
+ });
514
+ nodes.push(toGraphNode(styleDefinitionKey, 'style', styleDefinition.name, file.relPath, file, 'component-style-definitions', {
515
+ styleDefinition: {
516
+ kind: styleDefinition.kind,
517
+ variants: Object.keys(styleDefinition.variants),
518
+ classCount: styleDefinition.classes.length,
519
+ },
520
+ }));
521
+ edges.push(toGraphEdge(file.key, styleDefinitionKey, 'defines_style', 'component-style-definitions', file, 'high'));
522
+ }
523
+ }
524
+ addUiPatternRules(entries, usedKeys, nodes, edges, files);
525
+ addUiCompositionRules(entries, usedKeys, nodes, edges, files);
526
+ addConnectedPatternRules(entries, usedKeys, nodes, edges, reviewItems, files);
527
+ addRenderEdges(entries, files, edges);
528
+ addImportEdges(entries, files, edges, rootPath);
529
+ addApiCallEdges(entries, files, edges);
530
+ addStyleUsageEdges(entries, files, edges);
531
+ addModelUsageEdges(entries, files, edges);
532
+ addConceptClusters(entries, usedKeys, nodes, edges, reviewItems);
533
+ applyUsedBy(entries, edges);
534
+ return {
535
+ scanId: `${new Date().toISOString()}:${(0, utils_1.shortHash)(rootPath + entries.length)}`,
536
+ generatedAt: new Date().toISOString(),
537
+ rootPath,
538
+ projectType,
539
+ files,
540
+ entries: entries.sort((a, b) => a.key.localeCompare(b.key)),
541
+ nodes: (0, utils_1.uniqueBy)(nodes, (node) => node.k).sort((a, b) => a.k.localeCompare(b.k)),
542
+ edges: (0, utils_1.uniqueBy)(edges, (edge) => `${edge.from}:${edge.type}:${edge.to}`).sort((a, b) => `${a.from}:${a.type}:${a.to}`.localeCompare(`${b.from}:${b.type}:${b.to}`)),
543
+ reviewItems,
544
+ detected: detectProjectFacts(files),
545
+ };
546
+ }
547
+ function addEntry(entries, usedKeys, entry) {
548
+ let key = entry.key;
549
+ if (usedKeys.has(key)) {
550
+ key = `${key}.${(0, utils_1.shortHash)(String(entry.value.path ?? entry.summary))}`;
551
+ }
552
+ if (usedKeys.has(key)) {
553
+ return;
554
+ }
555
+ usedKeys.add(key);
556
+ entries.push({ ...entry, key });
557
+ }
558
+ function hasStyleSurface(file) {
559
+ return Boolean(file.cssVariables.length ||
560
+ file.cssClasses.length ||
561
+ file.classReferences.length ||
562
+ file.cssIds.length ||
563
+ file.cssVariableReferences.length ||
564
+ file.tailwindUtilities.length ||
565
+ file.uiElements.length ||
566
+ file.cssVariableValues.length ||
567
+ file.cssRules.length ||
568
+ file.classExpressions.length ||
569
+ file.componentStyleDefinitions.length ||
570
+ file.themeTokens.length ||
571
+ file.inlineStyles.length);
572
+ }
573
+ function isBackendRouteFile(file) {
574
+ return Boolean(file.layer === 'backend' ||
575
+ file.controllers.length ||
576
+ file.apiHandlers.length ||
577
+ file.backendEndpointContracts.length);
578
+ }
579
+ function backendFileContext(file) {
580
+ if (!file.backendEndpointContracts.length &&
581
+ !file.backendOperations.length &&
582
+ !file.backendSideEffects.length &&
583
+ !file.backendErrors.length &&
584
+ !file.backendLogging.length) {
585
+ return {};
586
+ }
587
+ return {
588
+ backend: {
589
+ endpointContracts: file.backendEndpointContracts,
590
+ operations: file.backendOperations,
591
+ sideEffects: uniqueSideEffectsForRecipe(file.backendSideEffects),
592
+ errors: file.backendErrors,
593
+ logging: file.backendLogging,
594
+ },
595
+ };
596
+ }
597
+ function findEndpointContract(file, handler) {
598
+ const [method, ...pathParts] = handler.split(/\s+/);
599
+ const handlerPath = pathParts.join(' ');
600
+ return file.backendEndpointContracts.find((contract) => {
601
+ if (contract.handler === handler) {
602
+ return true;
603
+ }
604
+ if (`${contract.method} ${contract.path}` === handler) {
605
+ return true;
606
+ }
607
+ return contract.method === method && [contract.path, contract.fullPath].includes(handlerPath);
608
+ });
609
+ }
610
+ function createBackendEndpointFlow(file, handlerKey, contract) {
611
+ return {
612
+ key: handlerKey,
613
+ framework: contract.framework,
614
+ endpoint: {
615
+ method: contract.method,
616
+ path: contract.fullPath ?? contract.path,
617
+ handlerName: contract.handlerName,
618
+ controller: contract.controller,
619
+ },
620
+ request: contract.request,
621
+ response: contract.response,
622
+ auth: contract.auth,
623
+ validation: contract.validation,
624
+ calls: contract.serviceCalls.map((call) => ({
625
+ ...call,
626
+ targetKey: backendCallKey(call),
627
+ })),
628
+ sideEffects: contract.sideEffects,
629
+ errors: contract.errors,
630
+ logging: contract.logging,
631
+ risk: contract.risk,
632
+ editScope: {
633
+ ownerFile: file.relPath,
634
+ openWhenEditing: (0, utils_1.unique)([
635
+ file.key,
636
+ ...backendCallDependencies(contract.serviceCalls),
637
+ ...file.imports
638
+ .filter((item) => /(dto|schema|validator|guard|policy|service|repository)/i.test(item))
639
+ .slice(0, 20),
640
+ ]),
641
+ },
642
+ };
643
+ }
644
+ function backendCallDependencies(calls) {
645
+ return (0, utils_1.unique)(calls
646
+ .filter((call) => ['service', 'repository', 'provider', 'client'].includes(call.kind))
647
+ .map(backendCallKey));
648
+ }
649
+ function addBackendCallEdges(fromKey, calls, file, edges) {
650
+ for (const call of calls) {
651
+ const targetKey = backendCallKey(call);
652
+ edges.push({
653
+ from: fromKey,
654
+ to: targetKey,
655
+ type: call.kind === 'repository' ? 'calls_repository' : 'calls',
656
+ confidence: call.kind === 'unknown' ? 'low' : 'medium',
657
+ source: sourceInfo('backend.contract.calls', call.kind === 'unknown' ? 'low' : 'medium', [`${fromKey} calls ${call.receiver}.${call.method}`], file),
658
+ });
659
+ }
660
+ }
661
+ function backendCallKey(call) {
662
+ return (0, utils_1.toRegistryKey)('service', (0, backend_1.backendReceiverToClassName)(call.receiver));
663
+ }
664
+ function uniqueSideEffectsForRecipe(sideEffects) {
665
+ return (0, utils_1.uniqueBy)(sideEffects, (sideEffect) => `${sideEffect.kind}.${sideEffect.target ?? ''}.${sideEffect.operation ?? ''}.${sideEffect.line}`);
666
+ }
667
+ function sourceInfo(extractor, confidence, evidence, file) {
668
+ return {
669
+ extractor,
670
+ confidence,
671
+ evidence,
672
+ path: file.relPath,
673
+ hash: file.sourceHash,
674
+ };
675
+ }
676
+ function toGraphNode(key, kind, name, relPath, file, extractor, facets) {
677
+ return {
678
+ k: key,
679
+ kind,
680
+ name,
681
+ path: relPath,
682
+ language: file.language,
683
+ framework: detectFrameworkForFile(file),
684
+ ...(facets ? { facets } : {}),
685
+ source: sourceInfo(extractor, 'high', [`path:${relPath}`], file),
686
+ };
687
+ }
688
+ function toGraphEdge(from, to, type, extractor, file, confidence = 'high') {
689
+ return {
690
+ from,
691
+ to,
692
+ type,
693
+ confidence,
694
+ source: sourceInfo(extractor, confidence, [`${from} ${type} ${to}`], file),
695
+ };
696
+ }
697
+ function addRenderEdges(entries, files, edges) {
698
+ const componentKeysByName = new Map();
699
+ for (const entry of entries.filter((item) => item.group === 'component')) {
700
+ const name = typeof entry.value.name === 'string' ? entry.value.name : undefined;
701
+ if (name) {
702
+ componentKeysByName.set(name, entry.key);
703
+ }
704
+ }
705
+ for (const file of files) {
706
+ if (!file.routes.length || !file.jsxElements.length) {
707
+ continue;
708
+ }
709
+ for (const route of file.routes) {
710
+ const routeKey = (0, utils_1.toRegistryKey)('route', route);
711
+ for (const jsxElement of file.jsxElements) {
712
+ const componentKey = componentKeysByName.get(jsxElement);
713
+ if (componentKey) {
714
+ edges.push(toGraphEdge(routeKey, componentKey, 'renders', 'react-jsx', file));
715
+ }
716
+ }
717
+ }
718
+ }
719
+ }
720
+ function addUiPatternRules(entries, usedKeys, nodes, edges, files) {
721
+ for (const file of files) {
722
+ const elementsByKind = (0, utils_1.groupBy)(file.uiElements, (element) => element.kind);
723
+ for (const [kind, elements] of elementsByKind) {
724
+ if (!elements.length || kind === 'unknown' || kind === 'link') {
725
+ continue;
726
+ }
727
+ const ruleKey = (0, utils_1.toRegistryKey)('rule', `ui.${kind}.${file.relPath.replace(/\.[^.]+$/, '')}`);
728
+ const styleKey = (0, utils_1.toRegistryKey)('style', file.relPath.replace(/\.[^.]+$/, ''));
729
+ const dependencies = (0, utils_1.unique)([file.key, styleKey]);
730
+ const classes = (0, utils_1.unique)(elements.flatMap((element) => element.classes)).slice(0, 80);
731
+ const unconditionalClasses = (0, utils_1.unique)(elements.flatMap((element) => element.defaultClasses ?? element.classes));
732
+ const conditionalClasses = (0, utils_1.unique)(elements.flatMap((element) => element.conditionalClasses ?? []));
733
+ const defaultClasses = (0, extractors_1.filterClasses)(unconditionalClasses, (className) => !(0, extractors_1.isDecorativeAccentClass)(className)).slice(0, 80);
734
+ const decorativeAccentClasses = (0, extractors_1.filterClasses)(classes, extractors_1.isDecorativeAccentClass).slice(0, 40);
735
+ const classGroups = (0, extractors_1.mergeClassGroups)(elements.map((element) => element.classGroups ?? {}));
736
+ const variants = (0, extractors_1.mergeVariantProps)(elements.map((element) => element.variants ?? {}));
737
+ addEntry(entries, usedKeys, {
738
+ key: ruleKey,
739
+ group: 'rule',
740
+ summary: `${(0, utils_1.titleCase)(kind)} UI pattern inferred from ${file.relPath}.`,
741
+ tags: (0, utils_1.unique)([
742
+ 'rule',
743
+ 'ui',
744
+ kind,
745
+ ...Object.keys(variants),
746
+ ...Object.values(variants).flatMap((values) => values),
747
+ ...defaultClasses.slice(0, 20),
748
+ ...(decorativeAccentClasses.length ? ['decorative-accent', 'highlight-only'] : []),
749
+ ]),
750
+ dependencies,
751
+ usedBy: [],
752
+ confidence: elements.length > 1 ? 'medium' : 'low',
753
+ status: 'inferred',
754
+ path: file.relPath,
755
+ value: {
756
+ name: `${(0, utils_1.titleCase)(kind)} pattern`,
757
+ kind: 'rule',
758
+ ruleType: 'ui-pattern',
759
+ concept: kind,
760
+ path: file.relPath,
761
+ classes,
762
+ defaultClasses,
763
+ conditionalClasses,
764
+ decorativeAccentClasses,
765
+ guidance: (0, extractors_1.createUiPatternGuidance)(kind, decorativeAccentClasses),
766
+ classGroups,
767
+ variants,
768
+ elements: elements.slice(0, 80),
769
+ sourceHash: file.sourceHash,
770
+ source: sourceInfo('ui.patterns', elements.length > 1 ? 'medium' : 'low', [`${kind}:${elements.length}`], file),
771
+ },
772
+ });
773
+ nodes.push(toGraphNode(ruleKey, 'rule', `${kind} pattern`, file.relPath, file, 'ui.patterns', {
774
+ ui: {
775
+ concept: kind,
776
+ classes: defaultClasses,
777
+ decorativeAccentClasses,
778
+ classGroups,
779
+ variants,
780
+ count: elements.length,
781
+ },
782
+ }));
783
+ edges.push(toGraphEdge(file.key, ruleKey, 'contains', 'ui.patterns', file, 'medium'));
784
+ }
785
+ }
786
+ }
787
+ function addUiCompositionRules(entries, usedKeys, nodes, edges, files) {
788
+ for (const file of files) {
789
+ if (!file.uiElements.length && !file.classExpressions.length) {
790
+ continue;
791
+ }
792
+ const ruleKey = (0, utils_1.toRegistryKey)('rule', `ui.composition.${file.relPath.replace(/\.[^.]+$/, '')}`);
793
+ const styleKey = (0, utils_1.toRegistryKey)('style', file.relPath.replace(/\.[^.]+$/, ''));
794
+ const componentKeys = file.components.map((component) => (0, utils_1.toRegistryKey)('component', component));
795
+ const routeKeys = file.routes.map((route) => (0, utils_1.toRegistryKey)('route', route));
796
+ const dependencies = (0, utils_1.unique)([
797
+ file.key,
798
+ ...(hasStyleSurface(file) ? [styleKey] : []),
799
+ ...componentKeys,
800
+ ...routeKeys,
801
+ ]);
802
+ const unconditionalClassReferences = (0, utils_1.unique)([
803
+ ...file.classExpressions.flatMap((expression) => expression.defaultClasses ?? []),
804
+ ...file.uiElements.flatMap((element) => element.defaultClasses ?? []),
805
+ ]);
806
+ const conditionalClasses = (0, utils_1.unique)([
807
+ ...file.classExpressions.flatMap((expression) => expression.conditionalClasses ?? []),
808
+ ...file.uiElements.flatMap((element) => element.conditionalClasses ?? []),
809
+ ]).slice(0, 120);
810
+ const baseClassReferences = unconditionalClassReferences.length
811
+ ? unconditionalClassReferences
812
+ : file.classReferences;
813
+ const responsiveClasses = (0, extractors_1.filterClasses)(baseClassReferences, extractors_1.isResponsiveClass).slice(0, 120);
814
+ const spacingClasses = (0, extractors_1.filterClasses)(baseClassReferences, extractors_1.isSpacingClass).slice(0, 120);
815
+ const surfaceClasses = (0, extractors_1.filterClasses)(baseClassReferences, (className) => (0, extractors_1.isSurfaceClass)(className) && !(0, extractors_1.isDecorativeAccentClass)(className)).slice(0, 120);
816
+ const decorativeAccentClasses = (0, extractors_1.filterClasses)(file.classReferences, extractors_1.isDecorativeAccentClass).slice(0, 60);
817
+ const typographyClasses = (0, extractors_1.filterClasses)(file.classReferences, extractors_1.isTypographyClass).slice(0, 120);
818
+ const compositionElements = file.uiElements.filter(extractors_1.isCompositionElement).slice(0, 90);
819
+ const classExpressionsByTarget = (0, extractors_1.groupClassExpressionsByTarget)(file.classExpressions);
820
+ const layoutRoles = (0, utils_1.unique)(file.uiElements.flatMap((element) => element.layoutRole ?? []));
821
+ const elementKinds = (0, utils_1.unique)(file.uiElements.map((element) => element.kind));
822
+ addEntry(entries, usedKeys, {
823
+ key: ruleKey,
824
+ group: 'rule',
825
+ summary: `UI composition contract for ${file.relPath}.`,
826
+ tags: (0, utils_1.unique)([
827
+ 'rule',
828
+ 'ui',
829
+ 'composition',
830
+ 'layout',
831
+ 'responsive',
832
+ file.kind,
833
+ file.layer,
834
+ file.language,
835
+ ...(0, utils_1.pathTerms)(file.relPath),
836
+ ...file.components,
837
+ ...file.routes,
838
+ ...elementKinds,
839
+ ...layoutRoles,
840
+ ...responsiveClasses.slice(0, 24),
841
+ ...surfaceClasses.slice(0, 24),
842
+ ...(decorativeAccentClasses.length ? ['decorative-accent', 'highlight-only'] : []),
843
+ ...spacingClasses.slice(0, 24),
844
+ ...typographyClasses.slice(0, 24),
845
+ ]),
846
+ dependencies,
847
+ usedBy: [],
848
+ confidence: file.components.length || file.routes.length ? 'high' : 'medium',
849
+ status: 'inferred',
850
+ path: file.relPath,
851
+ value: {
852
+ name: `${file.relPath} UI composition`,
853
+ kind: 'rule',
854
+ ruleType: 'ui-composition',
855
+ path: file.relPath,
856
+ components: file.components,
857
+ routes: file.routes,
858
+ imports: file.imports,
859
+ layoutRoles,
860
+ elementKinds,
861
+ guidance: (0, extractors_1.createUiCompositionGuidance)(file),
862
+ responsiveClasses,
863
+ spacingClasses,
864
+ surfaceClasses,
865
+ conditionalClasses,
866
+ decorativeAccentClasses,
867
+ typographyClasses,
868
+ componentSlots: (0, extractors_1.buildComponentSlots)(file.uiElements),
869
+ classExpressionsByTarget,
870
+ elements: compositionElements,
871
+ sourceHash: file.sourceHash,
872
+ source: sourceInfo('ui.composition', file.components.length || file.routes.length ? 'high' : 'medium', [
873
+ `path:${file.relPath}`,
874
+ `components:${file.components.join(',') || 'none'}`,
875
+ `uiElements:${file.uiElements.length}`,
876
+ ], file),
877
+ },
878
+ });
879
+ nodes.push(toGraphNode(ruleKey, 'rule', `${file.relPath} UI composition`, file.relPath, file, 'ui.composition', {
880
+ ui: {
881
+ ruleType: 'ui-composition',
882
+ components: file.components,
883
+ routes: file.routes,
884
+ layoutRoles,
885
+ elementKinds,
886
+ },
887
+ }));
888
+ edges.push(toGraphEdge(file.key, ruleKey, 'defines_ui_composition', 'ui.composition', file));
889
+ for (const componentKey of componentKeys) {
890
+ edges.push(toGraphEdge(componentKey, ruleKey, 'uses_ui_composition', 'ui.composition', file, 'high'));
891
+ }
892
+ for (const routeKey of routeKeys) {
893
+ edges.push(toGraphEdge(routeKey, ruleKey, 'uses_ui_composition', 'ui.composition', file, 'high'));
894
+ }
895
+ }
896
+ }
897
+ function addConnectedPatternRules(entries, usedKeys, nodes, edges, reviewItems, files) {
898
+ const clusters = buildConnectedPatternClusters(files);
899
+ for (const cluster of clusters) {
900
+ addEntry(entries, usedKeys, {
901
+ key: cluster.key,
902
+ group: 'cluster',
903
+ summary: cluster.summary,
904
+ tags: cluster.tags,
905
+ dependencies: cluster.dependencies,
906
+ usedBy: [],
907
+ confidence: cluster.confidence,
908
+ status: cluster.status,
909
+ path: cluster.instances[0]?.file.relPath,
910
+ value: cluster.value,
911
+ });
912
+ const first = cluster.instances[0]?.file;
913
+ nodes.push({
914
+ k: cluster.key,
915
+ kind: 'cluster',
916
+ name: cluster.name,
917
+ ...(first
918
+ ? {
919
+ path: first.relPath,
920
+ language: first.language,
921
+ framework: detectFrameworkForFile(first),
922
+ }
923
+ : {}),
924
+ facets: {
925
+ connectedPattern: {
926
+ domain: cluster.domain,
927
+ concept: cluster.concept,
928
+ score: Number(cluster.score.toFixed(2)),
929
+ usageCount: cluster.instances.length,
930
+ canonical: cluster.value.canonical,
931
+ },
932
+ },
933
+ source: {
934
+ extractor: 'connected.patterns',
935
+ confidence: cluster.confidence,
936
+ evidence: cluster.instances
937
+ .slice(0, 20)
938
+ .map((instance) => `${instance.evidence.path}${instance.evidence.line ? `:${instance.evidence.line}` : ''}`),
939
+ ...(first ? { path: first.relPath, hash: first.sourceHash } : {}),
940
+ },
941
+ });
942
+ for (const instance of cluster.instances.slice(0, 80)) {
943
+ edges.push(toGraphEdge(instance.file.key, cluster.key, 'contains_connected_pattern', 'connected.patterns', instance.file, cluster.confidence));
944
+ for (const graphKey of instance.graphKeys.slice(0, 24)) {
945
+ edges.push(toGraphEdge(graphKey, cluster.key, 'participates_in_connected_pattern', 'connected.patterns', instance.file, cluster.confidence));
946
+ }
947
+ }
948
+ }
949
+ for (const [conceptKey, conceptClusters] of (0, utils_1.groupBy)(clusters.filter((cluster) => cluster.domain === 'frontend' && cluster.score >= 6), (cluster) => `${cluster.domain}.${cluster.concept}`)) {
950
+ const ranked = [...conceptClusters].sort((a, b) => b.score - a.score);
951
+ const top = ranked[0];
952
+ const second = ranked[1];
953
+ if (!top || !second) {
954
+ continue;
955
+ }
956
+ const closeEnough = top.score - second.score <= 2.5;
957
+ const bothEstablished = top.instances.length >= 2 && second.instances.length >= 2;
958
+ if (closeEnough && bothEstablished) {
959
+ reviewItems.push({
960
+ id: `review.pattern.${conceptKey}.canonical`,
961
+ type: 'role_collision',
962
+ status: 'open',
963
+ group: 'cluster',
964
+ s: `Multiple ${top.concept} connected patterns look plausible. Name or choose the canonical roles after scan.`,
965
+ candidates: ranked.slice(0, 6).map((cluster) => cluster.key),
966
+ });
967
+ }
968
+ }
969
+ }
970
+ function buildConnectedPatternClusters(files) {
971
+ const instances = [
972
+ ...files.flatMap(createFrontendPatternInstances),
973
+ ...createBackendPatternInstances(files),
974
+ ...createDataPatternInstances(files),
975
+ ];
976
+ const grouped = (0, utils_1.groupBy)(instances, (instance) => `${instance.domain}:${instance.concept}:${instance.signature}`);
977
+ const clusters = [];
978
+ for (const group of grouped.values()) {
979
+ const first = group[0];
980
+ if (!first) {
981
+ continue;
982
+ }
983
+ const score = scoreConnectedPatternGroup(group);
984
+ const confidence = connectedPatternConfidence(score, group.length);
985
+ const dependencies = (0, utils_1.unique)(group.flatMap((instance) => [instance.file.key, ...instance.graphKeys])).slice(0, 160);
986
+ const examples = group.map((instance) => instance.evidence).slice(0, 24);
987
+ const defaultClasses = (0, utils_1.unique)(group.flatMap((instance) => instance.root?.defaultClasses ?? [])).slice(0, 120);
988
+ const conditionalClasses = (0, utils_1.unique)(group.flatMap((instance) => instance.root?.conditionalClasses ?? [])).slice(0, 80);
989
+ const decorativeAccentClasses = (0, utils_1.unique)(group.flatMap((instance) => instance.root?.decorativeAccentClasses ?? [])).slice(0, 60);
990
+ const classGroups = (0, extractors_1.mergeClassGroups)(group
991
+ .map((instance) => instance.root?.classGroups)
992
+ .filter((value) => Boolean(value)));
993
+ const childStructure = summarizeConnectedChildren(group);
994
+ const key = (0, utils_1.toRegistryKey)('cluster', `pattern.${first.domain}.${first.concept}.${connectedPatternSlug(first)}.${(0, utils_1.shortHash)(first.signature)}`);
995
+ const fileCount = new Set(group.map((instance) => instance.file.relPath)).size;
996
+ clusters.push({
997
+ domain: first.domain,
998
+ concept: first.concept,
999
+ signature: first.signature,
1000
+ key,
1001
+ name: `${(0, utils_1.titleCase)(first.concept)} connected pattern`,
1002
+ summary: `${(0, utils_1.titleCase)(first.concept)} connected ${first.domain} pattern seen ${group.length} time${group.length === 1 ? '' : 's'} across ${fileCount} file${fileCount === 1 ? '' : 's'}.`,
1003
+ score,
1004
+ confidence,
1005
+ status: 'inferred',
1006
+ instances: group,
1007
+ tags: (0, utils_1.unique)([
1008
+ 'cluster',
1009
+ 'pattern',
1010
+ 'connected-pattern',
1011
+ first.domain,
1012
+ first.concept,
1013
+ `usage-${group.length}`,
1014
+ confidence,
1015
+ ...group.flatMap((instance) => instance.tags).slice(0, 80),
1016
+ ...defaultClasses.slice(0, 24),
1017
+ ...(decorativeAccentClasses.length ? ['decorative-accent', 'highlight-only'] : []),
1018
+ ]),
1019
+ dependencies,
1020
+ value: {
1021
+ name: `${(0, utils_1.titleCase)(first.concept)} connected pattern`,
1022
+ kind: 'connected-pattern-cluster',
1023
+ patternKind: 'pattern_cluster',
1024
+ ruleType: 'connected-pattern',
1025
+ domain: first.domain,
1026
+ concept: first.concept,
1027
+ signature: first.signature,
1028
+ usageCount: group.length,
1029
+ fileCount,
1030
+ score: Number(score.toFixed(2)),
1031
+ confidence,
1032
+ canonical: false,
1033
+ defaultClasses,
1034
+ conditionalClasses,
1035
+ decorativeAccentClasses,
1036
+ classGroups,
1037
+ childStructure,
1038
+ examples,
1039
+ graphKeys: (0, utils_1.unique)(group.flatMap((instance) => instance.graphKeys)).slice(0, 80),
1040
+ guidance: createConnectedPatternGuidance(first, group, decorativeAccentClasses),
1041
+ variants: connectedPatternVariants(first.domain, group),
1042
+ children: connectedPatternChildren(first.domain, first.concept, group, dependencies),
1043
+ instances: group.map(compactConnectedPatternInstance).slice(0, 20),
1044
+ source: {
1045
+ extractor: 'connected.patterns',
1046
+ confidence,
1047
+ evidence: examples.map((example) => `${example.path}${example.line ? `:${example.line}` : ''}`),
1048
+ },
1049
+ },
1050
+ });
1051
+ }
1052
+ for (const sameConcept of (0, utils_1.groupBy)(clusters, (cluster) => `${cluster.domain}:${cluster.concept}`).values()) {
1053
+ const ranked = [...sameConcept].sort((a, b) => b.score - a.score);
1054
+ ranked.forEach((cluster, index) => {
1055
+ const canonical = index === 0;
1056
+ const nearestSibling = ranked[index === 0 ? 1 : 0];
1057
+ const conflictRisk = Boolean(nearestSibling &&
1058
+ Math.abs(cluster.score - nearestSibling.score) <= 2.5 &&
1059
+ cluster.instances.length >= 2 &&
1060
+ nearestSibling.instances.length >= 2);
1061
+ const scores = createConnectedPatternScores(cluster, index, ranked.length, conflictRisk);
1062
+ const decision = createConnectedPatternDecision(cluster, scores, index, ranked.length, conflictRisk);
1063
+ cluster.value.canonical = canonical;
1064
+ cluster.value.scores = scores;
1065
+ cluster.value.decision = decision;
1066
+ cluster.value.variants = connectedPatternVariants(cluster.domain, cluster.instances);
1067
+ cluster.value.children = connectedPatternChildren(cluster.domain, cluster.concept, cluster.instances, cluster.dependencies);
1068
+ if (canonical) {
1069
+ cluster.tags = (0, utils_1.unique)([...cluster.tags, 'canonical', 'highest-scoring']);
1070
+ cluster.value.guidance = (0, utils_1.unique)([
1071
+ ...(Array.isArray(cluster.value.guidance) ? cluster.value.guidance.map(String) : []),
1072
+ 'Prefer this pattern for new generation unless the task asks for a different named role or variant.',
1073
+ ]);
1074
+ }
1075
+ if (conflictRisk) {
1076
+ cluster.tags = (0, utils_1.unique)([...cluster.tags, 'needs-review', 'canonical-candidate']);
1077
+ cluster.value.review = 'canonical_conflict';
1078
+ }
1079
+ });
1080
+ }
1081
+ return clusters.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
1082
+ }
1083
+ function createFrontendPatternInstances(file) {
1084
+ const instances = [];
1085
+ for (const element of file.uiElements) {
1086
+ if (!isConnectedUiPatternRoot(element)) {
1087
+ continue;
1088
+ }
1089
+ const defaultClasses = (0, extractors_1.filterClasses)(element.defaultClasses?.length ? element.defaultClasses : element.classes, (className) => !(0, extractors_1.isDecorativeAccentClass)(className)).slice(0, 100);
1090
+ const conditionalClasses = (0, utils_1.unique)(element.conditionalClasses ?? []).slice(0, 80);
1091
+ const decorativeAccentClasses = (0, utils_1.unique)([
1092
+ ...(element.decorativeAccentClasses ?? []),
1093
+ ...(0, extractors_1.filterClasses)(element.classes, extractors_1.isDecorativeAccentClass),
1094
+ ]).slice(0, 60);
1095
+ const classGroups = (0, extractors_1.groupTailwindClasses)(defaultClasses.length ? defaultClasses : element.classes);
1096
+ const children = normalizeConnectedChildren(element.childSummary ?? []);
1097
+ const component = file.components[0];
1098
+ const route = file.routes[0];
1099
+ const graphKeys = (0, utils_1.unique)([
1100
+ ...(component ? [(0, utils_1.toRegistryKey)('component', component)] : []),
1101
+ ...(route ? [(0, utils_1.toRegistryKey)('route', route)] : []),
1102
+ file.key,
1103
+ ...(hasStyleSurface(file)
1104
+ ? [(0, utils_1.toRegistryKey)('style', file.relPath.replace(/\.[^.]+$/, ''))]
1105
+ : []),
1106
+ (0, utils_1.toRegistryKey)('rule', `ui.${element.kind}.${file.relPath.replace(/\.[^.]+$/, '')}`),
1107
+ (0, utils_1.toRegistryKey)('rule', `ui.composition.${file.relPath.replace(/\.[^.]+$/, '')}`),
1108
+ ]);
1109
+ const signature = [
1110
+ 'frontend',
1111
+ element.kind,
1112
+ normalizeLayoutRole(element.layoutRole),
1113
+ normalizeTagForSignature(element),
1114
+ classSignature(defaultClasses),
1115
+ children.map((child) => child.signature).join('|'),
1116
+ ].join(';');
1117
+ instances.push({
1118
+ domain: 'frontend',
1119
+ concept: element.kind,
1120
+ signature,
1121
+ summary: `${(0, utils_1.titleCase)(element.kind)} UI structure in ${file.relPath}.`,
1122
+ file,
1123
+ root: {
1124
+ kind: element.kind,
1125
+ tag: element.originalTag,
1126
+ layoutRole: element.layoutRole,
1127
+ classes: element.classes.slice(0, 120),
1128
+ defaultClasses,
1129
+ conditionalClasses,
1130
+ decorativeAccentClasses,
1131
+ classGroups,
1132
+ },
1133
+ children: children.map(withoutConnectedChildSignature),
1134
+ graphKeys,
1135
+ tags: (0, utils_1.unique)([
1136
+ 'ui',
1137
+ 'frontend',
1138
+ element.kind,
1139
+ normalizeLayoutRole(element.layoutRole),
1140
+ normalizeTagForSignature(element),
1141
+ ...(0, utils_1.pathTerms)(file.relPath),
1142
+ ...file.components,
1143
+ ...file.routes,
1144
+ ...Object.keys(classGroups),
1145
+ ]),
1146
+ evidence: {
1147
+ path: file.relPath,
1148
+ line: element.line,
1149
+ ...(component ? { component } : {}),
1150
+ ...(route ? { route } : {}),
1151
+ label: element.label,
1152
+ sourceHash: file.sourceHash,
1153
+ },
1154
+ value: {
1155
+ root: {
1156
+ kind: element.kind,
1157
+ tag: element.originalTag,
1158
+ layoutRole: element.layoutRole,
1159
+ defaultClasses,
1160
+ conditionalClasses,
1161
+ decorativeAccentClasses,
1162
+ classGroups,
1163
+ },
1164
+ children: children.map(withoutConnectedChildSignature),
1165
+ },
1166
+ baseWeight: frontendPatternWeight(file, element, children.length, defaultClasses),
1167
+ });
1168
+ }
1169
+ return instances;
1170
+ }
1171
+ function createBackendPatternInstances(files) {
1172
+ return files.flatMap((file) => {
1173
+ if ((0, extractors_1.isTestFile)(file.relPath) || /\.(md|mdx)$/i.test(file.relPath)) {
1174
+ return [];
1175
+ }
1176
+ if (!hasBackendPatternSurface(file)) {
1177
+ return [];
1178
+ }
1179
+ const concept = backendPatternConcept(file);
1180
+ const methodShapes = (0, utils_1.unique)(file.apiHandlers.map((handler) => handler.split(/\s+/)[0])).sort();
1181
+ const graphKeys = (0, utils_1.unique)([
1182
+ file.key,
1183
+ ...file.apiHandlers.map((handler) => (0, utils_1.toRegistryKey)('api', handler)),
1184
+ ...file.controllers.map((controller) => (0, utils_1.toRegistryKey)('controller', controller)),
1185
+ ...file.services.map((service) => (0, utils_1.toRegistryKey)('service', service)),
1186
+ ...file.modules.map((moduleName) => (0, utils_1.toRegistryKey)('module', moduleName)),
1187
+ ...file.models.map((model) => (0, utils_1.toRegistryKey)('model', model)),
1188
+ ...file.routes.map((route) => (0, utils_1.toRegistryKey)('route', route)),
1189
+ ]);
1190
+ const signature = [
1191
+ 'backend',
1192
+ concept,
1193
+ detectFrameworkForFile(file) ?? file.language,
1194
+ file.kind,
1195
+ `methods:${methodShapes.join(',') || 'none'}`,
1196
+ `controller:${Boolean(file.controllers.length)}`,
1197
+ `service:${Boolean(file.services.length)}`,
1198
+ `model:${Boolean(file.models.length)}`,
1199
+ `auth:${Boolean(file.authHints.length || file.middleware.length)}`,
1200
+ `events:${Boolean(file.events.length || file.jobs.length)}`,
1201
+ `calls:${Boolean(file.apiCalls.length)}`,
1202
+ ].join(';');
1203
+ return [
1204
+ {
1205
+ domain: 'backend',
1206
+ concept,
1207
+ signature,
1208
+ summary: `${(0, utils_1.titleCase)(concept)} backend flow in ${file.relPath}.`,
1209
+ file,
1210
+ graphKeys,
1211
+ tags: (0, utils_1.unique)([
1212
+ 'backend',
1213
+ concept,
1214
+ file.language,
1215
+ detectFrameworkForFile(file) ?? '',
1216
+ ...methodShapes,
1217
+ ...(file.authHints.length ? ['auth'] : []),
1218
+ ...(file.middleware.length ? ['middleware'] : []),
1219
+ ...(file.events.length ? ['event'] : []),
1220
+ ...(file.jobs.length ? ['job'] : []),
1221
+ ...(0, utils_1.pathTerms)(file.relPath),
1222
+ ]),
1223
+ evidence: {
1224
+ path: file.relPath,
1225
+ key: file.key,
1226
+ sourceHash: file.sourceHash,
1227
+ },
1228
+ value: {
1229
+ contracts: file.backendEndpointContracts,
1230
+ operations: file.backendOperations,
1231
+ apiHandlers: file.apiHandlers,
1232
+ controllers: file.controllers,
1233
+ services: file.services,
1234
+ modules: file.modules,
1235
+ routes: file.routes,
1236
+ models: file.models,
1237
+ apiCalls: file.apiCalls,
1238
+ envVars: file.envVars,
1239
+ authHints: file.authHints,
1240
+ middleware: file.middleware,
1241
+ jobs: file.jobs,
1242
+ events: file.events,
1243
+ sideEffects: uniqueSideEffectsForRecipe(file.backendSideEffects),
1244
+ errors: file.backendErrors,
1245
+ logging: file.backendLogging,
1246
+ risk: (0, utils_1.unique)([
1247
+ ...file.backendEndpointContracts.flatMap((contract) => contract.risk),
1248
+ ...file.backendOperations.flatMap((operation) => operation.risk),
1249
+ ]),
1250
+ imports: file.imports.slice(0, 60),
1251
+ },
1252
+ baseWeight: backendPatternWeight(file),
1253
+ },
1254
+ ];
1255
+ });
1256
+ }
1257
+ function createDataPatternInstances(files) {
1258
+ const instances = [];
1259
+ for (const file of files) {
1260
+ for (const model of file.models) {
1261
+ const fields = file.modelFields[model] ?? [];
1262
+ const fieldShape = summarizeModelFieldShape(fields);
1263
+ const signature = [
1264
+ 'data',
1265
+ 'model',
1266
+ detectFrameworkForFile(file) ?? file.language,
1267
+ fieldShape.signature,
1268
+ ].join(';');
1269
+ instances.push({
1270
+ domain: 'data',
1271
+ concept: 'model',
1272
+ signature,
1273
+ summary: `${model} data model shape in ${file.relPath}.`,
1274
+ file,
1275
+ graphKeys: (0, utils_1.unique)([file.key, (0, utils_1.toRegistryKey)('model', model)]),
1276
+ tags: (0, utils_1.unique)(['data', 'model', 'schema', model, file.language, ...fieldShape.tags]),
1277
+ evidence: {
1278
+ path: file.relPath,
1279
+ key: (0, utils_1.toRegistryKey)('model', model),
1280
+ sourceHash: file.sourceHash,
1281
+ },
1282
+ value: {
1283
+ model,
1284
+ fields,
1285
+ fieldShape,
1286
+ framework: detectFrameworkForFile(file),
1287
+ },
1288
+ baseWeight: 3 + Math.min(fields.length / 2, 8),
1289
+ });
1290
+ }
1291
+ }
1292
+ return instances;
1293
+ }
1294
+ function isConnectedUiPatternRoot(element) {
1295
+ if (element.kind === 'unknown' || element.kind === 'link' || element.kind === 'text') {
1296
+ return false;
1297
+ }
1298
+ if (['button', 'input', 'badge'].includes(element.kind)) {
1299
+ return Boolean(element.classes.length || element.defaultClasses?.length || element.variants);
1300
+ }
1301
+ return (0, extractors_1.isCompositionElement)(element) || Boolean(element.childSummary?.length);
1302
+ }
1303
+ function normalizeConnectedChildren(children) {
1304
+ return children
1305
+ .filter((child) => child.kind !== 'unknown' || child.classes.length || child.label)
1306
+ .slice(0, 10)
1307
+ .map((child) => {
1308
+ const defaultClasses = (0, extractors_1.filterClasses)(child.defaultClasses?.length ? child.defaultClasses : child.classes, (className) => !(0, extractors_1.isDecorativeAccentClass)(className)).slice(0, 48);
1309
+ const classGroups = (0, extractors_1.groupTailwindClasses)(defaultClasses);
1310
+ const normalizedKind = connectedChildKind(child);
1311
+ const signature = [
1312
+ normalizedKind,
1313
+ normalizeLayoutRole(child.layoutRole),
1314
+ normalizeChildTagForSignature(child),
1315
+ classSignature(defaultClasses),
1316
+ ].join(':');
1317
+ return {
1318
+ signature,
1319
+ kind: normalizedKind,
1320
+ sourceKind: child.kind,
1321
+ tag: child.originalTag,
1322
+ layoutRole: child.layoutRole,
1323
+ defaultClasses,
1324
+ conditionalClasses: child.conditionalClasses?.slice(0, 32) ?? [],
1325
+ classGroups,
1326
+ labelHint: child.label ? labelSignature(child.label) : undefined,
1327
+ };
1328
+ });
1329
+ }
1330
+ function withoutConnectedChildSignature(child) {
1331
+ return Object.fromEntries(Object.entries(child).filter(([key]) => key !== 'signature'));
1332
+ }
1333
+ function connectedChildKind(child) {
1334
+ if (child.layoutRole === 'eyebrow-or-badge') {
1335
+ return 'badge';
1336
+ }
1337
+ return child.kind;
1338
+ }
1339
+ function summarizeConnectedChildren(instances) {
1340
+ const childValues = instances.flatMap((instance) => instance.children ?? []);
1341
+ const grouped = (0, utils_1.groupBy)(childValues, (child) => [
1342
+ String(child.kind ?? ''),
1343
+ String(child.layoutRole ?? ''),
1344
+ JSON.stringify(child.classGroups ?? {}),
1345
+ ].join(':'));
1346
+ return Array.from(grouped.values())
1347
+ .map((children) => {
1348
+ const first = children[0] ?? {};
1349
+ return {
1350
+ kind: first.kind,
1351
+ tag: first.tag,
1352
+ layoutRole: first.layoutRole,
1353
+ count: children.length,
1354
+ defaultClasses: (0, utils_1.unique)(children.flatMap((child) => Array.isArray(child.defaultClasses)
1355
+ ? child.defaultClasses.filter((item) => typeof item === 'string')
1356
+ : [])).slice(0, 60),
1357
+ classGroups: (0, extractors_1.mergeClassGroups)(children
1358
+ .map((child) => child.classGroups)
1359
+ .filter((value) => Boolean(value))),
1360
+ };
1361
+ })
1362
+ .sort((a, b) => Number(b.count) - Number(a.count))
1363
+ .slice(0, 16);
1364
+ }
1365
+ function compactConnectedPatternInstance(instance) {
1366
+ return {
1367
+ path: instance.file.relPath,
1368
+ evidence: instance.evidence,
1369
+ graphKeys: instance.graphKeys.slice(0, 24),
1370
+ ...instance.value,
1371
+ };
1372
+ }
1373
+ function scoreConnectedPatternGroup(instances) {
1374
+ const usage = instances.length;
1375
+ const fileCount = new Set(instances.map((instance) => instance.file.relPath)).size;
1376
+ const componentCount = new Set(instances.flatMap((instance) => instance.file.components)).size;
1377
+ const routeCount = new Set(instances.flatMap((instance) => instance.file.routes)).size;
1378
+ const componentSourceBonus = instances.some((instance) => /(^|\/)(components|ui|shared|lib)\//.test(instance.file.relPath))
1379
+ ? 2
1380
+ : 0;
1381
+ const averageWeight = instances.reduce((total, instance) => total + instance.baseWeight, 0) / Math.max(usage, 1);
1382
+ return (usage * 3 +
1383
+ fileCount * 2 +
1384
+ componentCount * 1.5 +
1385
+ routeCount +
1386
+ componentSourceBonus +
1387
+ averageWeight);
1388
+ }
1389
+ function connectedPatternConfidence(score, usage) {
1390
+ if (usage >= 3 || score >= 16) {
1391
+ return 'high';
1392
+ }
1393
+ if (usage >= 2 || score >= 10) {
1394
+ return 'medium';
1395
+ }
1396
+ return 'low';
1397
+ }
1398
+ function createConnectedPatternScores(cluster, rank, siblingCount, conflictRisk) {
1399
+ const appearances = cluster.instances.length;
1400
+ const filePaths = (0, utils_1.unique)(cluster.instances.map((instance) => instance.file.relPath));
1401
+ const routeKeys = (0, utils_1.unique)(cluster.instances.flatMap((instance) => [
1402
+ ...instance.file.routes,
1403
+ ...(instance.evidence.route ? [instance.evidence.route] : []),
1404
+ ...instance.graphKeys.filter((key) => key.startsWith('route.')),
1405
+ ]));
1406
+ const componentKeys = (0, utils_1.unique)(cluster.instances.flatMap((instance) => [
1407
+ ...instance.file.components,
1408
+ ...(instance.evidence.component ? [instance.evidence.component] : []),
1409
+ ...instance.graphKeys.filter((key) => key.startsWith('component.')),
1410
+ ]));
1411
+ const classGroupCount = (0, utils_1.unique)(cluster.instances.flatMap((instance) => Object.keys(instance.root?.classGroups ?? {}))).length;
1412
+ const childKindCount = (0, utils_1.unique)(cluster.instances.flatMap((instance) => (instance.children ?? []).map((child) => String(child.kind ?? '')).filter(Boolean))).length;
1413
+ const repetition = (0, utils_1.clampPatternScore)(Math.log2(appearances + 1) / 4);
1414
+ const fileSpread = (0, utils_1.clampPatternScore)(Math.log2(filePaths.length + 1) / 3.5);
1415
+ const routeSpread = (0, utils_1.clampPatternScore)(Math.log2(routeKeys.length + 1) / 3);
1416
+ const dependencySpread = (0, utils_1.clampPatternScore)(Math.log2(cluster.dependencies.length + 1) / 5);
1417
+ const componentSpread = (0, utils_1.clampPatternScore)(Math.log2(componentKeys.length + 1) / 3.5);
1418
+ const scoreStrength = (0, utils_1.clampPatternScore)(cluster.score / 28);
1419
+ const rankSignal = rank === 0 ? 0.18 : Math.max(0, 0.1 - rank * 0.035) + (siblingCount <= 1 ? 0.05 : 0);
1420
+ const structuralSimilarity = roundPatternScore((0, utils_1.clampPatternScore)(0.52 +
1421
+ repetition * 0.2 +
1422
+ fileSpread * 0.12 +
1423
+ scoreStrength * 0.11 +
1424
+ (appearances > 1 ? 0.05 : 0)));
1425
+ const styleSimilarity = roundPatternScore((0, utils_1.clampPatternScore)(cluster.domain === 'frontend'
1426
+ ? 0.36 +
1427
+ (0, utils_1.clampPatternScore)(classGroupCount / 9) * 0.24 +
1428
+ repetition * 0.2 +
1429
+ fileSpread * 0.12 +
1430
+ componentSpread * 0.08
1431
+ : 0.5 + repetition * 0.18 + fileSpread * 0.12 + scoreStrength * 0.08));
1432
+ const behaviorSimilarity = roundPatternScore((0, utils_1.clampPatternScore)(cluster.domain === 'frontend'
1433
+ ? 0.38 +
1434
+ (0, utils_1.clampPatternScore)(childKindCount / 6) * 0.2 +
1435
+ routeSpread * 0.16 +
1436
+ componentSpread * 0.14 +
1437
+ repetition * 0.12
1438
+ : 0.48 +
1439
+ routeSpread * 0.2 +
1440
+ dependencySpread * 0.18 +
1441
+ repetition * 0.12 +
1442
+ scoreStrength * 0.1));
1443
+ const canonical = roundPatternScore((0, utils_1.clampPatternScore)(scoreStrength * 0.34 +
1444
+ repetition * 0.22 +
1445
+ fileSpread * 0.16 +
1446
+ routeSpread * 0.06 +
1447
+ dependencySpread * 0.08 +
1448
+ rankSignal -
1449
+ (appearances === 1 ? 0.18 : 0) -
1450
+ (conflictRisk ? 0.12 : 0)));
1451
+ const oneOffRisk = roundPatternScore((0, utils_1.clampPatternScore)((appearances === 1 ? 0.82 : 0.5) -
1452
+ canonical * 0.36 -
1453
+ repetition * 0.18 -
1454
+ fileSpread * 0.12 +
1455
+ (rank > 0 && siblingCount > 1 ? 0.12 : 0) +
1456
+ (conflictRisk ? 0.12 : 0)));
1457
+ return {
1458
+ appearances,
1459
+ files: filePaths.length,
1460
+ routes: routeKeys.length,
1461
+ owners: derivePatternOwners(filePaths),
1462
+ structuralSimilarity,
1463
+ styleSimilarity,
1464
+ behaviorSimilarity,
1465
+ canonical,
1466
+ oneOffRisk,
1467
+ };
1468
+ }
1469
+ function createConnectedPatternDecision(cluster, scores, rank, siblingCount, conflictRisk) {
1470
+ let reuse = 'allow';
1471
+ if (conflictRisk) {
1472
+ reuse = 'review';
1473
+ }
1474
+ else if (rank === 0 && scores.canonical >= 0.7 && scores.oneOffRisk <= 0.3) {
1475
+ reuse = 'prefer';
1476
+ }
1477
+ else if (siblingCount > 1 && scores.oneOffRisk >= 0.6) {
1478
+ reuse = 'avoid';
1479
+ }
1480
+ const readableConcept = readablePatternConcept(cluster.concept);
1481
+ const ownerScope = scores.owners.length ? scores.owners.slice(0, 3).join(', ') : cluster.domain;
1482
+ const scope = `${ownerScope} ${readableConcept} ${cluster.domain} pattern`;
1483
+ const reason = reuse === 'review'
1484
+ ? `This ${readableConcept} pattern is close to another established candidate, so a human should name or choose the canonical role.`
1485
+ : reuse === 'prefer'
1486
+ ? `Repeated ${scores.appearances} time${scores.appearances === 1 ? '' : 's'} across ${scores.files} file${scores.files === 1 ? '' : 's'} with low one-off risk.`
1487
+ : reuse === 'avoid'
1488
+ ? `Lower canonical score with high one-off risk; use only when editing this exact path or named variant.`
1489
+ : `Usable as a local variant; prefer a higher-scoring sibling when the request is broad.`;
1490
+ return { reuse, scope, reason };
1491
+ }
1492
+ function createConceptPatternScores(members, roleCandidates, shouldReview) {
1493
+ const appearances = members.length;
1494
+ const filePaths = (0, utils_1.unique)(members.flatMap((entry) => (entry.path ? [entry.path] : [])));
1495
+ const routeKeys = (0, utils_1.unique)(members.flatMap((entry) => [
1496
+ ...(entry.group === 'route' ? [entry.key] : []),
1497
+ ...(0, utils_1.stringArrayValue)(entry.value.routes),
1498
+ ...(0, utils_1.stringArrayValue)(entry.value.route ? [entry.value.route] : []),
1499
+ ]));
1500
+ const repetition = (0, utils_1.clampPatternScore)(Math.log2(appearances + 1) / 5);
1501
+ const fileSpread = (0, utils_1.clampPatternScore)(Math.log2(filePaths.length + 1) / 4);
1502
+ const routeSpread = (0, utils_1.clampPatternScore)(Math.log2(routeKeys.length + 1) / 3);
1503
+ const roleSpread = (0, utils_1.clampPatternScore)(Math.log2(roleCandidates.length + 1) / 3.5);
1504
+ const reviewPenalty = shouldReview ? 0.16 : 0;
1505
+ const canonical = roundPatternScore((0, utils_1.clampPatternScore)(0.34 +
1506
+ repetition * 0.24 +
1507
+ fileSpread * 0.18 +
1508
+ routeSpread * 0.08 +
1509
+ roleSpread * 0.1 -
1510
+ reviewPenalty));
1511
+ const oneOffRisk = roundPatternScore((0, utils_1.clampPatternScore)((appearances <= 1 ? 0.78 : 0.42) -
1512
+ repetition * 0.2 -
1513
+ fileSpread * 0.14 -
1514
+ canonical * 0.22 +
1515
+ (shouldReview ? 0.16 : 0)));
1516
+ return {
1517
+ appearances,
1518
+ files: filePaths.length,
1519
+ routes: routeKeys.length,
1520
+ owners: derivePatternOwners(filePaths),
1521
+ structuralSimilarity: roundPatternScore((0, utils_1.clampPatternScore)(0.45 + repetition * 0.26 + fileSpread * 0.16)),
1522
+ styleSimilarity: roundPatternScore((0, utils_1.clampPatternScore)(0.42 + roleSpread * 0.2 + repetition * 0.16)),
1523
+ behaviorSimilarity: roundPatternScore((0, utils_1.clampPatternScore)(0.38 + routeSpread * 0.22 + repetition * 0.18)),
1524
+ canonical,
1525
+ oneOffRisk,
1526
+ };
1527
+ }
1528
+ function createConceptPatternDecision(name, scores, shouldReview, roleCandidateCount) {
1529
+ const reuse = shouldReview
1530
+ ? 'review'
1531
+ : scores.canonical >= 0.68 && scores.oneOffRisk <= 0.3
1532
+ ? 'prefer'
1533
+ : scores.oneOffRisk >= 0.6
1534
+ ? 'avoid'
1535
+ : 'allow';
1536
+ const readableName = name.toLowerCase();
1537
+ const ownerScope = scores.owners.length ? scores.owners.slice(0, 3).join(', ') : 'codebase';
1538
+ const scope = `${ownerScope} ${readableName} lookup cluster`;
1539
+ const reason = reuse === 'review'
1540
+ ? `${roleCandidateCount} candidate ${readableName} recipes matched this lookup cluster; name or approve the canonical role before broad generation.`
1541
+ : reuse === 'prefer'
1542
+ ? `This lookup cluster has repeated evidence across ${scores.files} file${scores.files === 1 ? '' : 's'} with low one-off risk.`
1543
+ : reuse === 'avoid'
1544
+ ? `Too little repeated evidence; avoid for broad generation unless the task names this exact cluster.`
1545
+ : `Useful as a lookup cluster, but fetch the top connected pattern before generating new code.`;
1546
+ return { reuse, scope, reason };
1547
+ }
1548
+ function connectedPatternVariants(domain, instances) {
1549
+ const preferredPrefixes = domain === 'frontend'
1550
+ ? ['component.', 'route.', 'style.', 'rule.']
1551
+ : ['api.', 'controller.', 'service.', 'module.', 'model.', 'route.'];
1552
+ return (0, utils_1.unique)(instances.flatMap((instance) => [
1553
+ ...instance.file.components.map((component) => (0, utils_1.toRegistryKey)('component', component)),
1554
+ ...instance.file.routes.map((route) => (0, utils_1.toRegistryKey)('route', route)),
1555
+ ...instance.file.apiHandlers.map((handler) => (0, utils_1.toRegistryKey)('api', handler)),
1556
+ ...instance.file.controllers.map((controller) => (0, utils_1.toRegistryKey)('controller', controller)),
1557
+ ...instance.file.services.map((service) => (0, utils_1.toRegistryKey)('service', service)),
1558
+ ...instance.file.modules.map((moduleName) => (0, utils_1.toRegistryKey)('module', moduleName)),
1559
+ ...instance.file.models.map((model) => (0, utils_1.toRegistryKey)('model', model)),
1560
+ ...instance.graphKeys.filter((key) => preferredPrefixes.some((prefix) => key.startsWith(prefix))),
1561
+ ])).slice(0, 80);
1562
+ }
1563
+ function connectedPatternChildren(domain, concept, instances, dependencies) {
1564
+ if (domain === 'frontend') {
1565
+ const semanticChildren = (0, utils_1.unique)([
1566
+ `ui.${concept}`,
1567
+ ...instances.flatMap((instance) => (instance.children ?? [])
1568
+ .map((child) => String(child.kind ?? ''))
1569
+ .filter(Boolean)
1570
+ .map((kind) => `ui.${kind}`)),
1571
+ ]);
1572
+ const graphChildren = dependencies.filter((key) => ['component.', 'style.', 'rule.', 'route.'].some((prefix) => key.startsWith(prefix)));
1573
+ return (0, utils_1.unique)([...semanticChildren, ...graphChildren]).slice(0, 80);
1574
+ }
1575
+ return (0, utils_1.unique)(dependencies.filter((key) => ['api.', 'controller.', 'service.', 'module.', 'model.', 'route.', 'file.'].some((prefix) => key.startsWith(prefix)))).slice(0, 80);
1576
+ }
1577
+ function derivePatternOwners(filePaths) {
1578
+ const owners = new Set();
1579
+ for (const filePath of filePaths) {
1580
+ const lower = filePath.toLowerCase();
1581
+ if (/(^|\/)(admin|dashboard)(\/|$)/.test(lower))
1582
+ owners.add('admin');
1583
+ if (/auth|login|signin|signup|password|session|middleware/.test(lower))
1584
+ owners.add('auth');
1585
+ if (/contact|beta|sales|plans|pricing|about|learn|docs|updates|product|landing|home|marketing/.test(lower)) {
1586
+ owners.add('marketing');
1587
+ }
1588
+ if (/theme|theme-forge|tokens|color|font|radius/.test(lower))
1589
+ owners.add('theme');
1590
+ if (/component|component-forge|ui|design-system/.test(lower))
1591
+ owners.add('components');
1592
+ if (/mcp|codebase|scanner|recipe|manifest|graph/.test(lower))
1593
+ owners.add('mcp');
1594
+ if (/api|controller|service|module|prisma|database|schema|model|backend/.test(lower))
1595
+ owners.add('backend');
1596
+ if (!owners.size) {
1597
+ const firstSegment = lower.split('/').find(Boolean);
1598
+ if (firstSegment) {
1599
+ owners.add(firstSegment.replace(/[^a-z0-9-]+/g, '-'));
1600
+ }
1601
+ }
1602
+ }
1603
+ return Array.from(owners).sort().slice(0, 8);
1604
+ }
1605
+ function readablePatternConcept(value) {
1606
+ return value.replace(/[-_.]+/g, ' ').trim() || 'codebase';
1607
+ }
1608
+ function roundPatternScore(value) {
1609
+ return Number((0, utils_1.clampPatternScore)(value).toFixed(2));
1610
+ }
1611
+ function frontendPatternWeight(file, element, childCount, defaultClasses) {
1612
+ let weight = 1;
1613
+ if (file.components.length) {
1614
+ weight += 2;
1615
+ }
1616
+ if (file.routes.length) {
1617
+ weight += 1;
1618
+ }
1619
+ if (/(^|\/)(components|ui|shared|lib)\//.test(file.relPath)) {
1620
+ weight += 2;
1621
+ }
1622
+ if (['card', 'section', 'form', 'nav', 'button', 'input'].includes(element.kind)) {
1623
+ weight += 2;
1624
+ }
1625
+ if (element.layoutRole) {
1626
+ weight += 1;
1627
+ }
1628
+ weight += Math.min(childCount * 0.5, 3);
1629
+ weight += Math.min(Object.keys((0, extractors_1.groupTailwindClasses)(defaultClasses)).length * 0.3, 2);
1630
+ return weight;
1631
+ }
1632
+ function backendPatternWeight(file) {
1633
+ return (1 +
1634
+ file.apiHandlers.length * 1.2 +
1635
+ file.controllers.length * 2 +
1636
+ file.services.length * 2 +
1637
+ file.models.length * 1.5 +
1638
+ file.authHints.length * 0.5 +
1639
+ file.events.length * 0.5 +
1640
+ file.jobs.length * 0.5);
1641
+ }
1642
+ function hasBackendPatternSurface(file) {
1643
+ return Boolean(file.apiHandlers.length ||
1644
+ file.controllers.length ||
1645
+ file.services.length ||
1646
+ file.modules.length ||
1647
+ file.apiCalls.length ||
1648
+ file.middleware.length ||
1649
+ file.jobs.length ||
1650
+ file.events.length);
1651
+ }
1652
+ function backendPatternConcept(file) {
1653
+ if (file.controllers.length) {
1654
+ return file.services.length || file.models.length ? 'api-controller-flow' : 'api-controller';
1655
+ }
1656
+ if (file.apiHandlers.length) {
1657
+ return file.models.length || file.authHints.length ? 'api-data-flow' : 'api-route';
1658
+ }
1659
+ if (file.services.length) {
1660
+ return file.models.length ? 'service-data-flow' : 'service';
1661
+ }
1662
+ if (file.jobs.length || file.events.length) {
1663
+ return 'async-flow';
1664
+ }
1665
+ if (file.apiCalls.length) {
1666
+ return 'client-api-flow';
1667
+ }
1668
+ return 'backend-flow';
1669
+ }
1670
+ function summarizeModelFieldShape(fields) {
1671
+ const counts = {
1672
+ id: 0,
1673
+ unique: 0,
1674
+ relation: 0,
1675
+ optional: 0,
1676
+ datetime: 0,
1677
+ json: 0,
1678
+ string: 0,
1679
+ number: 0,
1680
+ boolean: 0,
1681
+ enum: 0,
1682
+ };
1683
+ for (const field of fields) {
1684
+ const lower = field.toLowerCase();
1685
+ if (/\b(?:id|uuid)\b|@id/.test(lower))
1686
+ counts.id += 1;
1687
+ if (/@unique|unique/.test(lower))
1688
+ counts.unique += 1;
1689
+ if (/@relation|\[\]|\bbelongs_to\b|\bhas_many\b|\bforeign\b/.test(lower))
1690
+ counts.relation += 1;
1691
+ if (/\?|\bnullable\b|null/.test(lower))
1692
+ counts.optional += 1;
1693
+ if (/\b(datetime|timestamp|date)\b/.test(lower))
1694
+ counts.datetime += 1;
1695
+ if (/\b(json|jsonb)\b/.test(lower))
1696
+ counts.json += 1;
1697
+ if (/\b(string|varchar|text|char)\b/.test(lower))
1698
+ counts.string += 1;
1699
+ if (/\b(int|integer|float|decimal|number|bigint)\b/.test(lower))
1700
+ counts.number += 1;
1701
+ if (/\b(bool|boolean)\b/.test(lower))
1702
+ counts.boolean += 1;
1703
+ if (/\benum\b/.test(lower))
1704
+ counts.enum += 1;
1705
+ }
1706
+ const tags = Object.entries(counts)
1707
+ .filter(([, count]) => count > 0)
1708
+ .map(([name]) => name);
1709
+ const signature = Object.entries(counts)
1710
+ .filter(([, count]) => count > 0)
1711
+ .map(([name, count]) => `${name}:${count}`)
1712
+ .join('|') || 'empty';
1713
+ return { signature, tags, counts };
1714
+ }
1715
+ function createConnectedPatternGuidance(first, instances, decorativeAccentClasses) {
1716
+ const guidance = [
1717
+ 'This is a connected pattern: preserve the root, child roles, default classes, and relationships together instead of copying isolated class tokens.',
1718
+ `Pattern score is based on usage count, file spread, component/route evidence, and structural specificity.`,
1719
+ ];
1720
+ if (first.domain === 'frontend') {
1721
+ guidance.push('Use defaultClasses for normal generation. Use childStructure to keep spacing, hierarchy, and slots aligned with the source pattern.');
1722
+ }
1723
+ if (decorativeAccentClasses.length) {
1724
+ guidance.push('decorativeAccentClasses are opt-in highlight treatments. They must not become default card/surface styling.');
1725
+ }
1726
+ if (instances.length === 1) {
1727
+ guidance.push('This pattern has one evidence instance; prefer higher-scoring sibling clusters when they answer the same lookup.');
1728
+ }
1729
+ if (first.domain === 'backend') {
1730
+ guidance.push('Follow graphKeys to see which API handlers, controllers, services, modules, and models participate in this backend flow.');
1731
+ }
1732
+ if (first.domain === 'data') {
1733
+ guidance.push('Use fieldShape for model-role lookup, then fetch the exact model recipe before writing migrations or queries.');
1734
+ }
1735
+ return guidance;
1736
+ }
1737
+ function connectedPatternSlug(instance) {
1738
+ const parts = [
1739
+ instance.concept,
1740
+ instance.root?.layoutRole,
1741
+ instance.root?.tag,
1742
+ ...Object.keys(instance.root?.classGroups ?? {}).slice(0, 3),
1743
+ ];
1744
+ return (parts
1745
+ .filter((part) => Boolean(part))
1746
+ .map((part) => part
1747
+ .toLowerCase()
1748
+ .replace(/[^a-z0-9]+/g, '-')
1749
+ .replace(/^-|-$/g, ''))
1750
+ .filter(Boolean)
1751
+ .slice(0, 5)
1752
+ .join('.') || instance.concept);
1753
+ }
1754
+ function classSignature(classes) {
1755
+ if (!classes.length) {
1756
+ return 'no-classes';
1757
+ }
1758
+ const grouped = (0, extractors_1.groupTailwindClasses)(classes);
1759
+ return Object.entries(grouped)
1760
+ .sort(([a], [b]) => a.localeCompare(b))
1761
+ .map(([group, values]) => `${group}=${values.slice(0, 16).sort().join(',')}`)
1762
+ .join('|');
1763
+ }
1764
+ function normalizeLayoutRole(value) {
1765
+ return value || 'unspecified-role';
1766
+ }
1767
+ function normalizeTagForSignature(element) {
1768
+ if (['card', 'button', 'input', 'form', 'nav', 'section', 'table', 'dialog', 'badge'].includes(element.kind)) {
1769
+ return element.kind;
1770
+ }
1771
+ return element.originalTag.toLowerCase();
1772
+ }
1773
+ function normalizeChildTagForSignature(child) {
1774
+ if (['button', 'input', 'badge', 'heading', 'text', 'link'].includes(child.kind)) {
1775
+ return child.kind;
1776
+ }
1777
+ return child.originalTag.toLowerCase();
1778
+ }
1779
+ function labelSignature(label) {
1780
+ return label
1781
+ .toLowerCase()
1782
+ .replace(/[^a-z0-9\s-]/g, '')
1783
+ .split(/\s+/)
1784
+ .filter((part) => part.length > 2 && !constants_1.classTokenStopWords.has(part))
1785
+ .slice(0, 4)
1786
+ .join('-');
1787
+ }
1788
+ function addImportEdges(entries, files, edges, rootPath) {
1789
+ const fileEntries = new Map(entries.filter((entry) => entry.group === 'file').map((entry) => [entry.path, entry]));
1790
+ const relPaths = new Set(files.map((file) => file.relPath));
1791
+ for (const file of files) {
1792
+ for (const importValue of file.imports) {
1793
+ if (!(0, utils_1.isRelativeImport)(importValue)) {
1794
+ continue;
1795
+ }
1796
+ const targetRelPath = (0, utils_1.resolveImportPath)(file.relPath, importValue, relPaths, rootPath);
1797
+ const targetEntry = targetRelPath ? fileEntries.get(targetRelPath) : undefined;
1798
+ if (targetEntry && targetEntry.key !== file.key) {
1799
+ edges.push(toGraphEdge(file.key, targetEntry.key, 'imports', 'core.imports', file, 'high'));
1800
+ }
1801
+ }
1802
+ }
1803
+ }
1804
+ function addApiCallEdges(entries, files, edges) {
1805
+ const apiEntriesByPath = new Map();
1806
+ for (const entry of entries.filter((item) => item.group === 'api')) {
1807
+ const handler = typeof entry.value.handler === 'string' ? entry.value.handler : '';
1808
+ const pathPart = handler.replace(/^[A-Z]+\s+/, '');
1809
+ if (pathPart) {
1810
+ apiEntriesByPath.set(pathPart, [...(apiEntriesByPath.get(pathPart) ?? []), entry]);
1811
+ }
1812
+ }
1813
+ for (const file of files) {
1814
+ for (const apiCall of file.apiCalls) {
1815
+ const targets = apiEntriesByPath.get(apiCall) ?? [];
1816
+ for (const target of targets) {
1817
+ if (target.path !== file.relPath) {
1818
+ edges.push(toGraphEdge(file.key, target.key, 'calls', 'api.calls', file, 'medium'));
1819
+ }
1820
+ }
1821
+ }
1822
+ }
1823
+ }
1824
+ function addStyleUsageEdges(entries, files, edges) {
1825
+ const styleEntries = entries.filter((entry) => entry.group === 'style');
1826
+ for (const file of files) {
1827
+ if (!file.classReferences.length && !file.tailwindUtilities.length) {
1828
+ continue;
1829
+ }
1830
+ const fileClasses = new Set(file.classReferences);
1831
+ for (const style of styleEntries) {
1832
+ const styleClasses = Array.isArray(style.value.cssClasses)
1833
+ ? style.value.cssClasses.filter((item) => typeof item === 'string')
1834
+ : [];
1835
+ const intersects = styleClasses.some((className) => fileClasses.has(className));
1836
+ const sameFile = style.path === file.relPath;
1837
+ if ((intersects || sameFile) && style.key !== file.key) {
1838
+ edges.push(toGraphEdge(file.key, style.key, 'styles', 'styles.usage', file, intersects ? 'high' : 'medium'));
1839
+ }
1840
+ }
1841
+ }
1842
+ }
1843
+ function addModelUsageEdges(entries, files, edges) {
1844
+ const modelEntries = entries.filter((entry) => entry.group === 'model');
1845
+ for (const file of files) {
1846
+ const haystack = [
1847
+ file.relPath,
1848
+ ...file.imports,
1849
+ ...file.exports,
1850
+ ...file.symbols,
1851
+ ...file.classes,
1852
+ ...file.functions,
1853
+ ...file.apiHandlers,
1854
+ ...file.services,
1855
+ ...file.controllers,
1856
+ ]
1857
+ .join(' ')
1858
+ .toLowerCase();
1859
+ for (const model of modelEntries) {
1860
+ const modelName = typeof model.value.name === 'string' ? model.value.name : '';
1861
+ if (!modelName || model.path === file.relPath) {
1862
+ continue;
1863
+ }
1864
+ if (new RegExp(`\\b${(0, utils_1.escapeRegExp)(modelName.toLowerCase())}\\b`).test(haystack)) {
1865
+ const type = /\b(create|insert|update|delete|save|write)\b/.test(haystack)
1866
+ ? 'writes'
1867
+ : 'reads';
1868
+ edges.push(toGraphEdge(file.key, model.key, type, 'dataflow.model-reference', file, 'low'));
1869
+ }
1870
+ }
1871
+ }
1872
+ }
1873
+ function addConceptClusters(entries, usedKeys, nodes, edges, reviewItems) {
1874
+ const concepts = [
1875
+ {
1876
+ key: 'cluster.ui.button',
1877
+ name: 'Buttons',
1878
+ terms: ['button', 'buttons', 'btn', 'cta', 'submit', 'primary action'],
1879
+ reviewId: 'review.ui.button.primary',
1880
+ reviewSummary: 'Multiple button-like patterns found. Canonical button roles need review.',
1881
+ },
1882
+ {
1883
+ key: 'cluster.ui.form',
1884
+ name: 'Forms',
1885
+ terms: ['form', 'forms', 'fieldset', 'form-field', 'textarea', 'select'],
1886
+ reviewId: 'review.ui.form.canonical',
1887
+ reviewSummary: 'Multiple form patterns found. Canonical form roles need review.',
1888
+ },
1889
+ {
1890
+ key: 'cluster.ui.input',
1891
+ name: 'Inputs',
1892
+ terms: ['input', 'textarea', 'select', 'checkbox', 'switch', 'slider', 'field', 'label'],
1893
+ reviewId: 'review.ui.input.canonical',
1894
+ reviewSummary: 'Multiple input patterns found. Canonical input roles need review.',
1895
+ },
1896
+ {
1897
+ key: 'cluster.ui.nav',
1898
+ name: 'Navigation',
1899
+ terms: ['nav', 'navbar', 'navigation', 'menu', 'sidebar', 'breadcrumb'],
1900
+ reviewId: 'review.ui.nav.canonical',
1901
+ reviewSummary: 'Multiple navigation patterns found. Canonical nav roles need review.',
1902
+ },
1903
+ {
1904
+ key: 'cluster.ui.card',
1905
+ name: 'Cards',
1906
+ terms: ['card', 'panel', 'tile', 'surface', 'section-frame', 'frame'],
1907
+ reviewId: 'review.ui.card.canonical',
1908
+ reviewSummary: 'Multiple card or panel patterns found. Canonical card roles need review.',
1909
+ },
1910
+ {
1911
+ key: 'cluster.ui.badge',
1912
+ name: 'Badges',
1913
+ terms: ['badge', 'chip', 'pill', 'status', 'tag', 'eyebrow'],
1914
+ reviewId: 'review.ui.badge.canonical',
1915
+ reviewSummary: 'Multiple badge-like patterns found. Canonical badge roles need review.',
1916
+ },
1917
+ {
1918
+ key: 'cluster.ui.dialog',
1919
+ name: 'Dialogs',
1920
+ terms: ['dialog', 'modal', 'popover', 'drawer', 'sheet'],
1921
+ reviewId: 'review.ui.dialog.canonical',
1922
+ reviewSummary: 'Multiple dialog-like patterns found. Canonical overlay roles need review.',
1923
+ },
1924
+ {
1925
+ key: 'cluster.ui.table',
1926
+ name: 'Tables',
1927
+ terms: ['table', 'thead', 'tbody', 'row', 'cell', 'data-grid', 'grid-table'],
1928
+ reviewId: 'review.ui.table.canonical',
1929
+ reviewSummary: 'Multiple table patterns found. Canonical table roles need review.',
1930
+ },
1931
+ {
1932
+ key: 'cluster.theme.tokens',
1933
+ name: 'Theme Tokens',
1934
+ terms: [
1935
+ 'theme',
1936
+ 'token',
1937
+ 'tokens',
1938
+ 'color',
1939
+ 'radius',
1940
+ 'spacing',
1941
+ 'font',
1942
+ 'surface',
1943
+ 'border',
1944
+ 'ring',
1945
+ 'shadow',
1946
+ ],
1947
+ reviewId: 'review.theme.tokens.canonical',
1948
+ reviewSummary: 'Multiple theme token sources found. Canonical theme source needs review.',
1949
+ },
1950
+ {
1951
+ key: 'cluster.domain.auth',
1952
+ name: 'Auth',
1953
+ terms: ['auth', 'login', 'signin', 'signup', 'session', 'permission', 'guard', 'middleware'],
1954
+ },
1955
+ {
1956
+ key: 'cluster.domain.user',
1957
+ name: 'Users',
1958
+ terms: ['user', 'users', 'account', 'profile', 'member'],
1959
+ },
1960
+ {
1961
+ key: 'cluster.domain.billing',
1962
+ name: 'Billing',
1963
+ terms: ['billing', 'invoice', 'payment', 'stripe', 'subscription', 'plan'],
1964
+ },
1965
+ {
1966
+ key: 'cluster.domain.settings',
1967
+ name: 'Settings',
1968
+ terms: ['settings', 'preferences', 'configuration', 'config'],
1969
+ },
1970
+ ];
1971
+ for (const concept of concepts) {
1972
+ const members = entries.filter((entry) => (0, extractors_1.entryMatchesConcept)(entry, concept.terms));
1973
+ if (!members.length) {
1974
+ continue;
1975
+ }
1976
+ const roleCandidates = members.filter((entry) => ['component', 'rule', 'style'].includes(entry.group));
1977
+ const dependencies = (0, utils_1.unique)(members.map((entry) => entry.key)).slice(0, 80);
1978
+ const shouldReview = Boolean(concept.reviewId && roleCandidates.length > 1);
1979
+ const status = shouldReview ? 'needs_review' : 'inferred';
1980
+ const scores = createConceptPatternScores(members, roleCandidates, shouldReview);
1981
+ const decision = createConceptPatternDecision(concept.name, scores, shouldReview, roleCandidates.length);
1982
+ addEntry(entries, usedKeys, {
1983
+ key: concept.key,
1984
+ group: 'cluster',
1985
+ summary: `${concept.name} patterns, rules, and related codebase facts.`,
1986
+ tags: (0, utils_1.unique)([...concept.terms, 'cluster', decision.reuse]),
1987
+ dependencies,
1988
+ usedBy: [],
1989
+ confidence: roleCandidates.length ? 'medium' : 'low',
1990
+ status,
1991
+ value: {
1992
+ name: concept.name,
1993
+ kind: 'cluster',
1994
+ patternKind: 'pattern_cluster',
1995
+ terms: concept.terms,
1996
+ members: dependencies,
1997
+ scores,
1998
+ decision,
1999
+ variants: roleCandidates.map((entry) => entry.key).slice(0, 40),
2000
+ children: dependencies,
2001
+ status,
2002
+ ...(shouldReview ? { reviewItem: concept.reviewId } : {}),
2003
+ },
2004
+ });
2005
+ nodes.push({
2006
+ k: concept.key,
2007
+ kind: 'cluster',
2008
+ name: concept.name,
2009
+ facets: {
2010
+ concept: {
2011
+ terms: concept.terms,
2012
+ members: dependencies.length,
2013
+ scores,
2014
+ decision,
2015
+ },
2016
+ },
2017
+ source: {
2018
+ extractor: 'concept.cluster',
2019
+ confidence: roleCandidates.length ? 'medium' : 'low',
2020
+ evidence: dependencies.slice(0, 30),
2021
+ },
2022
+ });
2023
+ for (const member of members.slice(0, 120)) {
2024
+ edges.push({
2025
+ from: concept.key,
2026
+ to: member.key,
2027
+ type: 'contains',
2028
+ confidence: 'medium',
2029
+ source: {
2030
+ extractor: 'concept.cluster',
2031
+ confidence: 'medium',
2032
+ evidence: [`terms:${concept.terms.join(',')}`],
2033
+ },
2034
+ });
2035
+ }
2036
+ if (shouldReview && concept.reviewId && concept.reviewSummary) {
2037
+ reviewItems.push({
2038
+ id: concept.reviewId,
2039
+ type: 'role_collision',
2040
+ status: 'open',
2041
+ group: 'cluster',
2042
+ s: concept.reviewSummary,
2043
+ candidates: (0, utils_1.unique)([concept.key, ...roleCandidates.map((entry) => entry.key)]).slice(0, 40),
2044
+ });
2045
+ }
2046
+ }
2047
+ }
2048
+ function applyUsedBy(entries, edges) {
2049
+ const entriesByKey = new Map(entries.map((entry) => [entry.key, entry]));
2050
+ for (const entry of entries) {
2051
+ for (const dependency of entry.dependencies) {
2052
+ const dependencyEntry = entriesByKey.get(dependency);
2053
+ if (dependencyEntry && !dependencyEntry.usedBy.includes(entry.key)) {
2054
+ dependencyEntry.usedBy.push(entry.key);
2055
+ }
2056
+ }
2057
+ }
2058
+ for (const edge of edges) {
2059
+ const targetEntry = entriesByKey.get(edge.to);
2060
+ if (targetEntry && !targetEntry.usedBy.includes(edge.from)) {
2061
+ targetEntry.usedBy.push(edge.from);
2062
+ }
2063
+ }
2064
+ for (const entry of entries) {
2065
+ entry.usedBy = (0, utils_1.unique)(entry.usedBy).sort();
2066
+ }
2067
+ }
2068
+ function detectProjectFacts(files) {
2069
+ const languages = (0, utils_1.unique)(files.map((file) => file.language)).sort();
2070
+ const dependencies = new Set(files.flatMap((file) => file.dependencies));
2071
+ const frameworks = new Set();
2072
+ if (dependencies.has('react') || files.some((file) => file.components.length)) {
2073
+ frameworks.add('react');
2074
+ }
2075
+ if (dependencies.has('next') ||
2076
+ files.some((file) => file.routes.length || file.kind.includes('next'))) {
2077
+ frameworks.add('nextjs');
2078
+ }
2079
+ if (Array.from(dependencies).some((dependency) => dependency.startsWith('@nestjs/')) ||
2080
+ files.some((file) => file.controllers.length || file.services.length || file.modules.length)) {
2081
+ frameworks.add('nestjs');
2082
+ }
2083
+ if (dependencies.has('prisma') ||
2084
+ dependencies.has('@prisma/client') ||
2085
+ files.some((file) => file.models.length)) {
2086
+ frameworks.add('prisma');
2087
+ }
2088
+ if (files.some((file) => file.language === 'css' || file.language === 'scss')) {
2089
+ frameworks.add('css');
2090
+ }
2091
+ if (files.some((file) => file.language === 'php' || file.language === 'blade.php')) {
2092
+ frameworks.add('php');
2093
+ }
2094
+ if (dependencies.has('laravel/framework') ||
2095
+ files.some((file) => file.relPath.startsWith('routes/') || file.relPath.includes('/Http/Controllers/'))) {
2096
+ frameworks.add('laravel');
2097
+ }
2098
+ if (files.some((file) => file.language === 'html' || file.kind === 'web-page')) {
2099
+ frameworks.add('raw-web');
2100
+ }
2101
+ if (files.some((file) => file.language === 'vue')) {
2102
+ frameworks.add('vue');
2103
+ }
2104
+ if (files.some((file) => file.language === 'svelte')) {
2105
+ frameworks.add('svelte');
2106
+ }
2107
+ if (files.some((file) => file.language === 'astro')) {
2108
+ frameworks.add('astro');
2109
+ }
2110
+ if (dependencies.has('express') || dependencies.has('fastify') || dependencies.has('hono')) {
2111
+ frameworks.add('node-http');
2112
+ }
2113
+ if (files.some((file) => file.language === 'py')) {
2114
+ frameworks.add('python');
2115
+ }
2116
+ if (files.some((file) => file.language === 'rb')) {
2117
+ frameworks.add('ruby');
2118
+ }
2119
+ if (files.some((file) => file.language === 'go')) {
2120
+ frameworks.add('go');
2121
+ }
2122
+ if (files.some((file) => file.language === 'sql')) {
2123
+ frameworks.add('sql');
2124
+ }
2125
+ return {
2126
+ languages,
2127
+ frameworks: Array.from(frameworks).sort(),
2128
+ };
2129
+ }
2130
+ function detectFrameworkForFile(file) {
2131
+ if (file.routes.length || file.kind.includes('next')) {
2132
+ return 'nextjs';
2133
+ }
2134
+ if (file.components.length || file.hooks.length) {
2135
+ return 'react';
2136
+ }
2137
+ if (file.controllers.length || file.services.length || file.modules.length) {
2138
+ return 'nestjs';
2139
+ }
2140
+ if (file.models.length) {
2141
+ return 'prisma';
2142
+ }
2143
+ if (file.kind.includes('style') || file.kind.includes('css')) {
2144
+ return 'css';
2145
+ }
2146
+ if (file.language === 'php' || file.language === 'blade.php') {
2147
+ return file.relPath.startsWith('routes/') || file.relPath.includes('/Http/')
2148
+ ? 'laravel'
2149
+ : 'php';
2150
+ }
2151
+ if (file.language === 'html' || file.kind === 'web-page') {
2152
+ return 'raw-web';
2153
+ }
2154
+ if (file.language === 'vue') {
2155
+ return 'vue';
2156
+ }
2157
+ if (file.language === 'svelte') {
2158
+ return 'sveltekit';
2159
+ }
2160
+ if (file.language === 'astro') {
2161
+ return 'astro';
2162
+ }
2163
+ if (file.language === 'py') {
2164
+ return 'python';
2165
+ }
2166
+ if (file.language === 'rb') {
2167
+ return 'ruby';
2168
+ }
2169
+ if (file.language === 'go') {
2170
+ return 'go';
2171
+ }
2172
+ if (file.language === 'sql') {
2173
+ return 'sql';
2174
+ }
2175
+ return undefined;
2176
+ }
2177
+ //# sourceMappingURL=bundle.js.map