proteum 2.1.2 → 2.1.6

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 (99) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +112 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/command.ts +8 -0
  15. package/cli/commands/connect.ts +45 -0
  16. package/cli/commands/dev.ts +26 -11
  17. package/cli/commands/diagnose.ts +286 -0
  18. package/cli/commands/doctor.ts +18 -5
  19. package/cli/commands/explain.ts +25 -0
  20. package/cli/commands/perf.ts +243 -0
  21. package/cli/commands/session.ts +254 -0
  22. package/cli/commands/sessionLocalRunner.js +188 -0
  23. package/cli/commands/trace.ts +17 -1
  24. package/cli/commands/verify.ts +281 -0
  25. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  26. package/cli/compiler/artifacts/controllers.ts +198 -49
  27. package/cli/compiler/artifacts/discovery.ts +0 -34
  28. package/cli/compiler/artifacts/manifest.ts +90 -6
  29. package/cli/compiler/artifacts/routing.ts +2 -2
  30. package/cli/compiler/artifacts/services.ts +277 -130
  31. package/cli/compiler/client/index.ts +3 -0
  32. package/cli/compiler/common/files/style.ts +52 -0
  33. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  34. package/cli/compiler/common/scripts.ts +11 -5
  35. package/cli/compiler/index.ts +2 -1
  36. package/cli/compiler/server/index.ts +3 -0
  37. package/cli/presentation/commands.ts +136 -7
  38. package/cli/presentation/devSession.ts +32 -7
  39. package/cli/runtime/commands.ts +193 -6
  40. package/cli/scaffold/index.ts +14 -25
  41. package/cli/scaffold/templates.ts +41 -27
  42. package/cli/utils/agents.ts +4 -2
  43. package/cli/utils/keyboard.ts +8 -0
  44. package/client/dev/profiler/ApexChart.tsx +66 -0
  45. package/client/dev/profiler/index.tsx +2798 -417
  46. package/client/dev/profiler/runtime.noop.ts +12 -0
  47. package/client/dev/profiler/runtime.ts +195 -4
  48. package/client/services/router/request/api.ts +6 -1
  49. package/common/applicationConfig.ts +173 -0
  50. package/common/applicationConfigLoader.ts +102 -0
  51. package/common/connectedProjects.ts +113 -0
  52. package/common/dev/connect.ts +267 -0
  53. package/common/dev/console.ts +31 -0
  54. package/common/dev/contractsDoctor.ts +128 -0
  55. package/common/dev/diagnostics.ts +59 -15
  56. package/common/dev/inspection.ts +491 -0
  57. package/common/dev/performance.ts +809 -0
  58. package/common/dev/profiler.ts +3 -0
  59. package/common/dev/proteumManifest.ts +31 -6
  60. package/common/dev/requestTrace.ts +56 -1
  61. package/common/dev/session.ts +24 -0
  62. package/common/env/proteumEnv.ts +176 -50
  63. package/common/router/index.ts +1 -0
  64. package/common/router/request/api.ts +2 -0
  65. package/config.ts +5 -0
  66. package/docs/dev-commands.md +5 -1
  67. package/docs/dev-sessions.md +90 -0
  68. package/docs/diagnostics.md +74 -11
  69. package/docs/request-tracing.md +50 -3
  70. package/package.json +1 -1
  71. package/server/app/container/config.ts +16 -87
  72. package/server/app/container/console/index.ts +42 -8
  73. package/server/app/container/index.ts +3 -1
  74. package/server/app/container/trace/index.ts +153 -0
  75. package/server/app/devDiagnostics.ts +138 -0
  76. package/server/app/index.ts +18 -8
  77. package/server/app/service/container.ts +0 -12
  78. package/server/app/service/index.ts +0 -2
  79. package/server/services/prisma/index.ts +121 -4
  80. package/server/services/router/http/index.ts +352 -0
  81. package/server/services/router/index.ts +50 -47
  82. package/server/services/router/request/api.ts +160 -19
  83. package/server/services/router/request/index.ts +8 -0
  84. package/server/services/router/response/index.ts +24 -1
  85. package/server/services/router/response/page/document.tsx +5 -0
  86. package/server/services/router/response/page/index.tsx +10 -0
  87. package/agents/framework/AGENTS.md +0 -177
  88. package/server/services/auth/router/service.json +0 -6
  89. package/server/services/auth/service.json +0 -6
  90. package/server/services/cron/service.json +0 -6
  91. package/server/services/disks/drivers/local/service.json +0 -6
  92. package/server/services/disks/drivers/s3/service.json +0 -6
  93. package/server/services/disks/service.json +0 -6
  94. package/server/services/fetch/service.json +0 -7
  95. package/server/services/prisma/service.json +0 -6
  96. package/server/services/router/service.json +0 -6
  97. package/server/services/schema/router/service.json +0 -6
  98. package/server/services/schema/service.json +0 -6
  99. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  TProteumManifest,
3
3
  TProteumManifestCommand,
4
+ TProteumManifestConnectedProject,
4
5
  TProteumManifestController,
5
6
  TProteumManifestDiagnostic,
6
7
  TProteumManifestLayout,
@@ -8,7 +9,7 @@ import type {
8
9
  TProteumManifestService,
9
10
  } from './proteumManifest';
10
11
 
11
- export const explainSectionNames = ['app', 'conventions', 'env', 'services', 'controllers', 'commands', 'routes', 'layouts', 'diagnostics'] as const;
12
+ export const explainSectionNames = ['app', 'conventions', 'env', 'connected', 'services', 'controllers', 'commands', 'routes', 'layouts', 'diagnostics'] as const;
12
13
 
13
14
  export type TExplainSectionName = (typeof explainSectionNames)[number];
14
15
  export type THumanTextBlock = {
@@ -51,12 +52,15 @@ const formatServiceItem = (manifest: TProteumManifest, service: TProteumManifest
51
52
  return `${service.registeredName} -> ref ${service.refTo} [${service.parent}]`;
52
53
  }
53
54
 
54
- const source = service.metasFilepath ? formatManifestFilepath(manifest, service.metasFilepath) : 'unknown';
55
- return `${service.registeredName} -> ${service.id} (${service.metaName}) [${service.scope}] priority=${service.priority} source=${source}`;
55
+ const source = service.sourceFilepath ? formatManifestFilepath(manifest, service.sourceFilepath) : 'unknown';
56
+ return `${service.registeredName} -> ${service.className || 'unknown'} [${service.scope}] priority=${service.priority} parent=${service.parent} source=${source}`;
56
57
  };
57
58
 
59
+ const formatConnectedProjectItem = (_manifest: TProteumManifest, connectedProject: TProteumManifestConnectedProject) =>
60
+ `${connectedProject.namespace} -> ${connectedProject.identityIdentifier || connectedProject.packageName || 'unknown'} controllers=${connectedProject.controllerCount} internal=${connectedProject.urlInternal || 'missing'}${connectedProject.sourceKind ? ` source=${connectedProject.sourceKind}` : ''}${connectedProject.sourceValue ? ` sourceValue=${connectedProject.sourceValue}` : ' source=missing'}${connectedProject.typingMode ? ` typing=${connectedProject.typingMode}` : ''}${connectedProject.cachedContractFilepath ? ` contract=${formatManifestFilepath(_manifest, connectedProject.cachedContractFilepath)}` : ''}`;
61
+
58
62
  const formatControllerItem = (manifest: TProteumManifest, controller: TProteumManifestController) =>
59
- `${controller.clientAccessor} -> POST ${controller.httpPath} [${controller.scope}] input=${controller.hasInput ? 'yes' : 'no'} source=${formatManifestFilepath(manifest, controller.filepath)}${formatManifestLocation(controller.sourceLocation.line, controller.sourceLocation.column)}#${controller.methodName}`;
63
+ `${controller.clientAccessor} -> POST ${controller.httpPath} [${controller.scope}] input=${controller.hasInput ? 'yes' : 'no'}${controller.connectedProjectNamespace ? ` connected=${controller.connectedProjectNamespace}` : ''} source=${formatManifestFilepath(manifest, controller.filepath)}${formatManifestLocation(controller.sourceLocation.line, controller.sourceLocation.column)}#${controller.methodName}`;
60
64
 
61
65
  const formatCommandItem = (manifest: TProteumManifest, command: TProteumManifestCommand) =>
62
66
  `${command.path} -> ${command.className}.${command.methodName} [${command.scope}] source=${formatManifestFilepath(manifest, command.filepath)}${formatManifestLocation(command.sourceLocation.line, command.sourceLocation.column)}`;
@@ -90,13 +94,18 @@ const formatDiagnosticItem = (manifest: TProteumManifest, diagnostic: TProteumMa
90
94
  export const pickExplainManifestSections = (manifest: TProteumManifest, sectionNames: TExplainSectionName[]) => {
91
95
  if (sectionNames.length === 0) return manifest;
92
96
 
93
- const selected: Partial<TProteumManifest> = {};
97
+ const selected: Record<string, unknown> = {};
94
98
 
95
99
  for (const sectionName of sectionNames) {
96
- selected[sectionName] = manifest[sectionName];
100
+ if (sectionName === 'connected') {
101
+ selected.connectedProjects = manifest.connectedProjects;
102
+ continue;
103
+ }
104
+
105
+ selected[sectionName] = manifest[sectionName as keyof TProteumManifest];
97
106
  }
98
107
 
99
- return selected;
108
+ return selected as Partial<TProteumManifest>;
100
109
  };
101
110
 
102
111
  export const buildExplainSummaryItems = (manifest: TProteumManifest) => {
@@ -108,6 +117,7 @@ export const buildExplainSummaryItems = (manifest: TProteumManifest) => {
108
117
  `Proteum manifest: ${formatManifestFilepath(manifest, `${normalizePath(manifest.app.root)}/.proteum/manifest.json`)}`,
109
118
  `App: ${manifest.app.identity.name} (${manifest.app.identity.identifier})`,
110
119
  `Env vars: ${providedRequiredEnvVariables}/${manifest.env.requiredVariables.length} required provided`,
120
+ `Connected projects: ${manifest.connectedProjects.length}`,
111
121
  `Services: ${manifest.services.app.length} app, ${manifest.services.routerPlugins.length} router plugins`,
112
122
  `Controllers: ${manifest.controllers.length}`,
113
123
  `Commands: ${manifest.commands.length}`,
@@ -129,9 +139,12 @@ export const buildExplainBlocks = (manifest: TProteumManifest, sectionNames: TEx
129
139
  `root=${formatManifestFilepath(manifest, manifest.app.root)}`,
130
140
  `coreRoot=${formatManifestFilepath(manifest, manifest.app.coreRoot)}`,
131
141
  `identity=${formatManifestFilepath(manifest, manifest.app.identityFilepath)}`,
142
+ `setup=${formatManifestFilepath(manifest, manifest.app.setupFilepath)}`,
132
143
  `name=${manifest.app.identity.name}`,
133
144
  `identifier=${manifest.app.identity.identifier}`,
134
145
  `title=${manifest.app.identity.fullTitle || manifest.app.identity.title || manifest.app.identity.name}`,
146
+ `transpile=${manifest.app.setup.transpile?.join(', ') || 'none'}`,
147
+ `connect=${Object.keys(manifest.app.setup.connect || {}).join(', ') || 'none'}`,
135
148
  ],
136
149
  });
137
150
  continue;
@@ -162,11 +175,20 @@ export const buildExplainBlocks = (manifest: TProteumManifest, sectionNames: TEx
162
175
  `resolved.profile=${manifest.env.resolved.profile}`,
163
176
  `resolved.routerPort=${manifest.env.resolved.routerPort}`,
164
177
  `resolved.routerCurrentDomain=${manifest.env.resolved.routerCurrentDomain}`,
178
+ `resolved.routerInternalUrl=${manifest.env.resolved.routerInternalUrl}`,
165
179
  ],
166
180
  });
167
181
  continue;
168
182
  }
169
183
 
184
+ if (sectionName === 'connected') {
185
+ blocks.push({
186
+ title: 'Connected Projects',
187
+ items: manifest.connectedProjects.map((connectedProject) => formatConnectedProjectItem(manifest, connectedProject)),
188
+ });
189
+ continue;
190
+ }
191
+
170
192
  if (sectionName === 'services') {
171
193
  blocks.push(
172
194
  {
@@ -254,9 +276,12 @@ export const buildDoctorResponse = (manifest: TProteumManifest, strict = false):
254
276
  };
255
277
  };
256
278
 
257
- export const buildDoctorBlocks = (manifest: TProteumManifest): THumanTextBlock[] => {
258
- const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error');
259
- const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning');
279
+ export const buildDoctorBlocksFromDiagnostics = (
280
+ manifest: TProteumManifest,
281
+ diagnostics: TProteumManifestDiagnostic[],
282
+ ): THumanTextBlock[] => {
283
+ const errors = diagnostics.filter((diagnostic) => diagnostic.level === 'error');
284
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.level === 'warning');
260
285
 
261
286
  return [
262
287
  {
@@ -284,15 +309,34 @@ export const buildDoctorBlocks = (manifest: TProteumManifest): THumanTextBlock[]
284
309
  ];
285
310
  };
286
311
 
287
- export const renderDoctorHuman = (manifest: TProteumManifest, strict = false) => {
288
- const response = buildDoctorResponse(manifest, strict);
289
- if (response.diagnostics.length === 0) return 'Proteum doctor\n- No manifest diagnostics were found.';
312
+ export const buildDoctorBlocks = (manifest: TProteumManifest) => buildDoctorBlocksFromDiagnostics(manifest, manifest.diagnostics);
313
+
314
+ export const renderDoctorResponseHuman = ({
315
+ manifest,
316
+ response,
317
+ title,
318
+ emptyMessage,
319
+ }: {
320
+ manifest: TProteumManifest;
321
+ response: TDoctorResponse;
322
+ title: string;
323
+ emptyMessage?: string;
324
+ }) => {
325
+ if (response.diagnostics.length === 0) return `${title}\n- ${emptyMessage || 'No manifest diagnostics were found.'}`;
290
326
 
291
327
  return [
292
- 'Proteum doctor',
328
+ title,
293
329
  `- ${response.summary.errors} errors`,
294
330
  `- ${response.summary.warnings} warnings`,
295
331
  '',
296
- ...buildDoctorBlocks(manifest).map(renderHumanBlock),
332
+ ...buildDoctorBlocksFromDiagnostics(manifest, response.diagnostics).map(renderHumanBlock),
297
333
  ].join('\n');
298
334
  };
335
+
336
+ export const renderDoctorHuman = (manifest: TProteumManifest, strict = false) =>
337
+ renderDoctorResponseHuman({
338
+ emptyMessage: 'No manifest diagnostics were found.',
339
+ manifest,
340
+ response: buildDoctorResponse(manifest, strict),
341
+ title: 'Proteum doctor',
342
+ });
@@ -0,0 +1,491 @@
1
+ import { buildExplainSummaryItems, type TDoctorResponse } from './diagnostics';
2
+ import type { TDevConsoleLogsResponse } from './console';
3
+ import type {
4
+ TProteumManifest,
5
+ TProteumManifestCommand,
6
+ TProteumManifestController,
7
+ TProteumManifestDiagnostic,
8
+ TProteumManifestLayout,
9
+ TProteumManifestRoute,
10
+ TProteumManifestScope,
11
+ TProteumManifestService,
12
+ TProteumManifestSourceLocation,
13
+ } from './proteumManifest';
14
+ import type { TRequestTrace, TTraceEvent } from './requestTrace';
15
+
16
+ /*----------------------------------
17
+ - TYPES
18
+ ----------------------------------*/
19
+
20
+ export type TOwnerKind = 'route' | 'controller' | 'command' | 'service' | 'layout' | 'diagnostic';
21
+
22
+ export type TOwnerSource = {
23
+ filepath: string;
24
+ line?: number;
25
+ column?: number;
26
+ scope?: TProteumManifestScope;
27
+ };
28
+
29
+ export type TExplainOwnerMatch = {
30
+ details: string[];
31
+ kind: TOwnerKind;
32
+ label: string;
33
+ matchedOn: string[];
34
+ score: number;
35
+ source: TOwnerSource;
36
+ };
37
+
38
+ export type TExplainOwnerResponse = {
39
+ matches: TExplainOwnerMatch[];
40
+ normalizedQuery: string;
41
+ query: string;
42
+ };
43
+
44
+ export type TTraceAttributionItem = {
45
+ kind: 'request' | 'event' | 'call' | 'sql';
46
+ label: string;
47
+ owner?: TExplainOwnerMatch;
48
+ reference: string;
49
+ };
50
+
51
+ export type TTraceAttributionResponse = {
52
+ calls: TTraceAttributionItem[];
53
+ events: TTraceAttributionItem[];
54
+ primary?: TTraceAttributionItem;
55
+ sqlQueries: TTraceAttributionItem[];
56
+ };
57
+
58
+ export type TDiagnoseSuspect = {
59
+ filepath: string;
60
+ label: string;
61
+ line?: number;
62
+ column?: number;
63
+ reasons: string[];
64
+ score: number;
65
+ };
66
+
67
+ export type TDiagnoseResponse = {
68
+ attribution?: TTraceAttributionResponse;
69
+ contracts: TDoctorResponse;
70
+ doctor: TDoctorResponse;
71
+ explainSummary: string[];
72
+ owner: TExplainOwnerResponse;
73
+ query: string;
74
+ request?: TRequestTrace;
75
+ serverLogs: TDevConsoleLogsResponse;
76
+ suspects: TDiagnoseSuspect[];
77
+ };
78
+
79
+ type TManifestEntry =
80
+ | { details: string[]; kind: 'command'; label: string; searchTerms: string[]; source: TOwnerSource }
81
+ | { details: string[]; kind: 'controller'; label: string; searchTerms: string[]; source: TOwnerSource }
82
+ | { details: string[]; kind: 'diagnostic'; label: string; searchTerms: string[]; source: TOwnerSource }
83
+ | { details: string[]; kind: 'layout'; label: string; searchTerms: string[]; source: TOwnerSource }
84
+ | { details: string[]; kind: 'route'; label: string; searchTerms: string[]; source: TOwnerSource }
85
+ | { details: string[]; kind: 'service'; label: string; searchTerms: string[]; source: TOwnerSource };
86
+
87
+ type TSuspectAccumulator = {
88
+ column?: number;
89
+ filepath: string;
90
+ label: string;
91
+ line?: number;
92
+ reasons: Set<string>;
93
+ score: number;
94
+ };
95
+
96
+ /*----------------------------------
97
+ - CONSTANTS
98
+ ----------------------------------*/
99
+
100
+ const normalizeText = (value: string) => value.trim().replace(/\\/g, '/').toLowerCase();
101
+ const tokenize = (value: string) =>
102
+ normalizeText(value)
103
+ .split(/[^a-z0-9/_.-]+/i)
104
+ .map((token) => token.trim())
105
+ .filter(Boolean);
106
+
107
+ /*----------------------------------
108
+ - HELPERS
109
+ ----------------------------------*/
110
+
111
+ const toSource = (filepath: string, sourceLocation?: TProteumManifestSourceLocation, scope?: TProteumManifestScope): TOwnerSource => ({
112
+ filepath,
113
+ line: sourceLocation?.line,
114
+ column: sourceLocation?.column,
115
+ ...(scope ? { scope } : {}),
116
+ });
117
+
118
+ const pushTerm = (terms: Set<string>, value?: string) => {
119
+ if (!value) return;
120
+
121
+ const normalized = normalizeText(value);
122
+ if (!normalized) return;
123
+ terms.add(normalized);
124
+
125
+ const basename = normalized.split('/').pop();
126
+ if (basename) terms.add(basename);
127
+
128
+ for (const token of tokenize(value)) terms.add(token);
129
+ };
130
+
131
+ const createRouteEntry = (route: TProteumManifestRoute): TManifestEntry => {
132
+ const terms = new Set<string>();
133
+
134
+ pushTerm(terms, route.filepath);
135
+ pushTerm(terms, route.path);
136
+ pushTerm(terms, route.pathRaw);
137
+ pushTerm(terms, route.codeRaw);
138
+ pushTerm(terms, route.chunkId);
139
+ pushTerm(terms, route.chunkFilepath);
140
+ pushTerm(terms, route.kind);
141
+ pushTerm(terms, route.methodName);
142
+
143
+ return {
144
+ details: [
145
+ `${route.kind} ${route.methodName}`,
146
+ ...(route.path ? [`path=${route.path}`] : []),
147
+ ...(route.chunkId ? [`chunk=${route.chunkId}`] : []),
148
+ `setup=${route.hasSetup ? 'yes' : 'no'}`,
149
+ ],
150
+ kind: 'route',
151
+ label: route.path || route.pathRaw || route.chunkId || route.filepath,
152
+ searchTerms: [...terms],
153
+ source: toSource(route.filepath, route.sourceLocation, route.scope),
154
+ };
155
+ };
156
+
157
+ const createControllerEntry = (controller: TProteumManifestController): TManifestEntry => {
158
+ const terms = new Set<string>();
159
+
160
+ pushTerm(terms, controller.filepath);
161
+ pushTerm(terms, controller.className);
162
+ pushTerm(terms, controller.methodName);
163
+ pushTerm(terms, controller.routePath);
164
+ pushTerm(terms, controller.httpPath);
165
+ pushTerm(terms, controller.clientAccessor);
166
+
167
+ return {
168
+ details: [
169
+ controller.className,
170
+ `method=${controller.methodName}`,
171
+ `http=${controller.httpPath}`,
172
+ `client=${controller.clientAccessor}`,
173
+ ],
174
+ kind: 'controller',
175
+ label: controller.httpPath,
176
+ searchTerms: [...terms],
177
+ source: toSource(controller.filepath, controller.sourceLocation, controller.scope),
178
+ };
179
+ };
180
+
181
+ const createCommandEntry = (command: TProteumManifestCommand): TManifestEntry => {
182
+ const terms = new Set<string>();
183
+
184
+ pushTerm(terms, command.filepath);
185
+ pushTerm(terms, command.className);
186
+ pushTerm(terms, command.methodName);
187
+ pushTerm(terms, command.path);
188
+
189
+ return {
190
+ details: [command.className, `method=${command.methodName}`, `path=${command.path}`],
191
+ kind: 'command',
192
+ label: command.path,
193
+ searchTerms: [...terms],
194
+ source: toSource(command.filepath, command.sourceLocation, command.scope),
195
+ };
196
+ };
197
+
198
+ const createServiceEntry = (service: TProteumManifestService): TManifestEntry => {
199
+ const terms = new Set<string>();
200
+ const sourceFilepath = service.sourceFilepath || '';
201
+
202
+ pushTerm(terms, service.registeredName);
203
+ pushTerm(terms, service.className);
204
+ pushTerm(terms, service.parent);
205
+ pushTerm(terms, service.refTo);
206
+ pushTerm(terms, service.importPath);
207
+ pushTerm(terms, sourceFilepath);
208
+
209
+ return {
210
+ details: [
211
+ service.kind,
212
+ `registered=${service.registeredName}`,
213
+ ...(service.className ? [`class=${service.className}`] : []),
214
+ ...(service.importPath ? [`import=${service.importPath}`] : []),
215
+ ...(service.refTo ? [`ref=${service.refTo}`] : []),
216
+ ],
217
+ kind: 'service',
218
+ label: service.registeredName,
219
+ searchTerms: [...terms],
220
+ source: toSource(sourceFilepath || service.importPath || service.registeredName, undefined, service.scope),
221
+ };
222
+ };
223
+
224
+ const createLayoutEntry = (layout: TProteumManifestLayout): TManifestEntry => {
225
+ const terms = new Set<string>();
226
+
227
+ pushTerm(terms, layout.filepath);
228
+ pushTerm(terms, layout.chunkId);
229
+ pushTerm(terms, layout.importPath);
230
+
231
+ return {
232
+ details: [`chunk=${layout.chunkId}`, `depth=${layout.depth}`],
233
+ kind: 'layout',
234
+ label: layout.chunkId || layout.filepath,
235
+ searchTerms: [...terms],
236
+ source: toSource(layout.filepath, undefined, layout.scope),
237
+ };
238
+ };
239
+
240
+ const createDiagnosticEntry = (diagnostic: TProteumManifestDiagnostic): TManifestEntry => {
241
+ const terms = new Set<string>();
242
+
243
+ pushTerm(terms, diagnostic.code);
244
+ pushTerm(terms, diagnostic.message);
245
+ pushTerm(terms, diagnostic.filepath);
246
+ for (const related of diagnostic.relatedFilepaths || []) pushTerm(terms, related);
247
+
248
+ return {
249
+ details: [`[${diagnostic.level}]`, diagnostic.message, ...(diagnostic.relatedFilepaths || []).map((filepath) => `related=${filepath}`)],
250
+ kind: 'diagnostic',
251
+ label: diagnostic.code,
252
+ searchTerms: [...terms],
253
+ source: toSource(diagnostic.filepath, diagnostic.sourceLocation),
254
+ };
255
+ };
256
+
257
+ const buildManifestEntries = (manifest: TProteumManifest) => [
258
+ ...manifest.routes.client.map(createRouteEntry),
259
+ ...manifest.routes.server.map(createRouteEntry),
260
+ ...manifest.controllers.map(createControllerEntry),
261
+ ...manifest.commands.map(createCommandEntry),
262
+ ...manifest.services.app.map(createServiceEntry),
263
+ ...manifest.services.routerPlugins.map(createServiceEntry),
264
+ ...manifest.layouts.map(createLayoutEntry),
265
+ ...manifest.diagnostics.map(createDiagnosticEntry),
266
+ ];
267
+
268
+ const scoreOwnerMatch = (query: string, entry: TManifestEntry) => {
269
+ const normalizedQuery = normalizeText(query);
270
+ const queryTokens = tokenize(query);
271
+ let score = 0;
272
+ const matchedOn: string[] = [];
273
+
274
+ for (const term of entry.searchTerms) {
275
+ if (term === normalizedQuery) {
276
+ score += 120;
277
+ matchedOn.push(term);
278
+ continue;
279
+ }
280
+
281
+ if (normalizedQuery.length >= 3 && term.includes(normalizedQuery)) {
282
+ score += 40;
283
+ matchedOn.push(term);
284
+ }
285
+ }
286
+
287
+ for (const token of queryTokens) {
288
+ if (entry.searchTerms.some((term) => term === token)) {
289
+ score += 18;
290
+ matchedOn.push(token);
291
+ continue;
292
+ }
293
+
294
+ if (token.length >= 3 && entry.searchTerms.some((term) => term.includes(token))) {
295
+ score += 8;
296
+ matchedOn.push(token);
297
+ }
298
+ }
299
+
300
+ if ((entry.kind === 'controller' && entry.label === query) || (entry.kind === 'route' && entry.label === query)) score += 30;
301
+
302
+ return {
303
+ matchedOn: [...new Set(matchedOn)],
304
+ score,
305
+ };
306
+ };
307
+
308
+ const toOwnerMatch = (entry: TManifestEntry, score: number, matchedOn: string[]): TExplainOwnerMatch => ({
309
+ details: entry.details,
310
+ kind: entry.kind,
311
+ label: entry.label,
312
+ matchedOn,
313
+ score,
314
+ source: entry.source,
315
+ });
316
+
317
+ const summarizeTraceStatus = (statusCode?: number, errorMessage?: string) => {
318
+ if (errorMessage) return errorMessage;
319
+ if (statusCode === undefined) return 'pending';
320
+ return String(statusCode);
321
+ };
322
+
323
+ const buildTraceItemOwner = (
324
+ manifest: TProteumManifest,
325
+ query: string,
326
+ kind: TTraceAttributionItem['kind'],
327
+ reference: string,
328
+ label: string,
329
+ ) => {
330
+ const owner = explainOwner(manifest, query).matches[0];
331
+ return {
332
+ kind,
333
+ label,
334
+ owner,
335
+ reference,
336
+ } satisfies TTraceAttributionItem;
337
+ };
338
+
339
+ const getEventQuery = (event: TTraceEvent) => {
340
+ const source = event.details.source;
341
+ if (typeof source === 'object' && source && 'entries' in source) {
342
+ const filepath = source.entries.filepath;
343
+ if (typeof filepath === 'string') return filepath;
344
+ }
345
+
346
+ const routePath = event.details.routePath;
347
+ if (typeof routePath === 'string' && routePath) return routePath;
348
+
349
+ const target = event.details.target;
350
+ if (typeof target === 'string' && target) return target;
351
+
352
+ const filepath = event.details.filepath;
353
+ if (typeof filepath === 'string' && filepath) return filepath;
354
+
355
+ return '';
356
+ };
357
+
358
+ const addSuspect = (
359
+ suspects: Map<string, TSuspectAccumulator>,
360
+ owner: TExplainOwnerMatch | undefined,
361
+ weight: number,
362
+ reason: string,
363
+ ) => {
364
+ if (!owner) return;
365
+
366
+ const key = `${owner.source.filepath}:${owner.source.line || 0}:${owner.source.column || 0}`;
367
+ const existing = suspects.get(key);
368
+
369
+ if (existing) {
370
+ existing.score += weight;
371
+ existing.reasons.add(reason);
372
+ return;
373
+ }
374
+
375
+ suspects.set(key, {
376
+ column: owner.source.column,
377
+ filepath: owner.source.filepath,
378
+ label: owner.label,
379
+ line: owner.source.line,
380
+ reasons: new Set([reason]),
381
+ score: weight,
382
+ });
383
+ };
384
+
385
+ /*----------------------------------
386
+ - PUBLIC API
387
+ ----------------------------------*/
388
+
389
+ export const explainOwner = (manifest: TProteumManifest, query: string): TExplainOwnerResponse => {
390
+ const normalizedQuery = normalizeText(query);
391
+ if (!normalizedQuery) return { matches: [], normalizedQuery, query };
392
+
393
+ const matches = buildManifestEntries(manifest)
394
+ .map((entry) => {
395
+ const { score, matchedOn } = scoreOwnerMatch(query, entry);
396
+ return score > 0 ? toOwnerMatch(entry, score, matchedOn) : undefined;
397
+ })
398
+ .filter((match): match is TExplainOwnerMatch => match !== undefined)
399
+ .sort((left, right) => right.score - left.score || left.kind.localeCompare(right.kind) || left.label.localeCompare(right.label))
400
+ .slice(0, 12);
401
+
402
+ return { matches, normalizedQuery, query };
403
+ };
404
+
405
+ export const buildTraceAttribution = (manifest: TProteumManifest, request?: TRequestTrace): TTraceAttributionResponse | undefined => {
406
+ if (!request) return undefined;
407
+
408
+ const primaryQuery = request.path;
409
+ const primary = primaryQuery
410
+ ? buildTraceItemOwner(manifest, primaryQuery, 'request', request.id, `${request.method} ${request.path}`)
411
+ : undefined;
412
+
413
+ return {
414
+ calls: request.calls.flatMap((call) => {
415
+ const query = call.path || call.label;
416
+ return query
417
+ ? [buildTraceItemOwner(manifest, query, 'call', call.id, `${call.method || ''} ${call.path || call.label}`.trim())]
418
+ : [];
419
+ }),
420
+ events: request.events.flatMap((event) => {
421
+ const query = getEventQuery(event);
422
+ return query ? [buildTraceItemOwner(manifest, query, 'event', `${event.index}`, `${event.type}`)] : [];
423
+ }),
424
+ primary,
425
+ sqlQueries: request.sqlQueries.flatMap((query) => {
426
+ const ownerQuery = query.callerPath || query.callerLabel || query.query;
427
+ return ownerQuery
428
+ ? [buildTraceItemOwner(manifest, ownerQuery, 'sql', query.id, `${query.operation} ${query.model || query.callerPath || 'query'}`)]
429
+ : [];
430
+ }),
431
+ };
432
+ };
433
+
434
+ export const buildDiagnoseResponse = ({
435
+ contracts,
436
+ doctor,
437
+ manifest,
438
+ query,
439
+ request,
440
+ serverLogs,
441
+ }: {
442
+ contracts: TDoctorResponse;
443
+ doctor: TDoctorResponse;
444
+ manifest: TProteumManifest;
445
+ query: string;
446
+ request?: TRequestTrace;
447
+ serverLogs: TDevConsoleLogsResponse;
448
+ }): TDiagnoseResponse => {
449
+ const owner = explainOwner(manifest, query);
450
+ const attribution = buildTraceAttribution(manifest, request);
451
+ const suspects = new Map<string, TSuspectAccumulator>();
452
+
453
+ addSuspect(suspects, owner.matches[0], 5, 'owner query');
454
+ addSuspect(suspects, attribution?.primary?.owner, 8, 'primary request');
455
+
456
+ for (const item of attribution?.events || []) addSuspect(suspects, item.owner, 2, `event:${item.label}`);
457
+ for (const item of attribution?.calls || []) addSuspect(suspects, item.owner, 3, `call:${item.label}`);
458
+ for (const item of attribution?.sqlQueries || []) addSuspect(suspects, item.owner, 1, `sql:${item.label}`);
459
+
460
+ for (const diagnostic of [...doctor.diagnostics, ...contracts.diagnostics]) {
461
+ const topOwner = explainOwner(manifest, diagnostic.filepath).matches[0];
462
+ addSuspect(suspects, topOwner, diagnostic.level === 'error' ? 4 : 2, diagnostic.code);
463
+ }
464
+
465
+ return {
466
+ attribution,
467
+ contracts,
468
+ doctor,
469
+ explainSummary: buildExplainSummaryItems(manifest),
470
+ owner,
471
+ query,
472
+ request,
473
+ serverLogs,
474
+ suspects: [...suspects.values()]
475
+ .sort((left, right) => right.score - left.score || left.filepath.localeCompare(right.filepath))
476
+ .slice(0, 12)
477
+ .map((suspect) => ({
478
+ column: suspect.column,
479
+ filepath: suspect.filepath,
480
+ label: suspect.label,
481
+ line: suspect.line,
482
+ reasons: [...suspect.reasons],
483
+ score: suspect.score,
484
+ })),
485
+ };
486
+ };
487
+
488
+ export const summarizeTraceForDiagnose = (request?: TRequestTrace) =>
489
+ !request
490
+ ? 'No request trace matched the diagnose query.'
491
+ : `${request.method} ${request.path} status=${summarizeTraceStatus(request.statusCode, request.errorMessage)} capture=${request.capture} calls=${request.calls.length} sql=${request.sqlQueries.length} events=${request.events.length}`;