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.
- package/AGENTS.md +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- 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.
|
|
55
|
-
return `${service.registeredName} -> ${service.
|
|
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:
|
|
97
|
+
const selected: Record<string, unknown> = {};
|
|
94
98
|
|
|
95
99
|
for (const sectionName of sectionNames) {
|
|
96
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
328
|
+
title,
|
|
293
329
|
`- ${response.summary.errors} errors`,
|
|
294
330
|
`- ${response.summary.warnings} warnings`,
|
|
295
331
|
'',
|
|
296
|
-
...
|
|
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}`;
|