proteum 2.1.9 → 2.2.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 (84) hide show
  1. package/.codex/environments/environment.toml +11 -0
  2. package/AGENTS.md +27 -11
  3. package/README.md +30 -11
  4. package/agents/project/AGENTS.md +172 -123
  5. package/agents/project/CODING_STYLE.md +1 -1
  6. package/agents/project/app-root/AGENTS.md +16 -0
  7. package/agents/project/client/AGENTS.md +5 -5
  8. package/agents/project/client/pages/AGENTS.md +13 -13
  9. package/agents/project/diagnostics.md +19 -10
  10. package/agents/project/optimizations.md +5 -6
  11. package/agents/project/root/AGENTS.md +297 -0
  12. package/agents/project/server/routes/AGENTS.md +2 -2
  13. package/agents/project/server/services/AGENTS.md +4 -2
  14. package/agents/project/tests/AGENTS.md +9 -2
  15. package/cli/app/index.ts +31 -7
  16. package/cli/commands/configure.ts +226 -0
  17. package/cli/commands/dev.ts +0 -2
  18. package/cli/commands/diagnose.ts +33 -1
  19. package/cli/commands/explain.ts +1 -1
  20. package/cli/commands/migrate.ts +51 -0
  21. package/cli/commands/orient.ts +169 -0
  22. package/cli/commands/perf.ts +8 -1
  23. package/cli/commands/verify.ts +1003 -49
  24. package/cli/compiler/artifacts/manifest.ts +4 -4
  25. package/cli/compiler/artifacts/routing.ts +2 -2
  26. package/cli/compiler/artifacts/services.ts +12 -3
  27. package/cli/compiler/client/index.ts +65 -19
  28. package/cli/compiler/common/files/style.ts +47 -2
  29. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  30. package/cli/compiler/common/index.ts +10 -0
  31. package/cli/compiler/common/proteumManifest.ts +1 -0
  32. package/cli/compiler/server/index.ts +34 -9
  33. package/cli/context.ts +6 -1
  34. package/cli/index.ts +7 -8
  35. package/cli/migrate/pageContract.ts +516 -0
  36. package/cli/paths.ts +47 -6
  37. package/cli/presentation/commands.ts +100 -10
  38. package/cli/presentation/devSession.ts +4 -6
  39. package/cli/presentation/help.ts +2 -2
  40. package/cli/presentation/ink.ts +10 -5
  41. package/cli/presentation/welcome.ts +2 -4
  42. package/cli/runtime/commands.ts +94 -1
  43. package/cli/scaffold/index.ts +2 -2
  44. package/cli/scaffold/templates.ts +4 -2
  45. package/cli/utils/agents.ts +273 -58
  46. package/client/dev/profiler/index.tsx +3 -2
  47. package/client/router.ts +10 -2
  48. package/client/services/router/index.tsx +6 -22
  49. package/common/dev/connect.ts +20 -4
  50. package/common/dev/console.ts +7 -0
  51. package/common/dev/contractsDoctor.ts +354 -0
  52. package/common/dev/diagnostics.ts +10 -7
  53. package/common/dev/inspection.ts +830 -38
  54. package/common/dev/performance.ts +19 -5
  55. package/common/dev/profiler.ts +1 -0
  56. package/common/dev/proteumManifest.ts +5 -4
  57. package/common/dev/requestTrace.ts +78 -1
  58. package/common/env/proteumEnv.ts +10 -3
  59. package/common/router/contracts.ts +8 -11
  60. package/common/router/index.ts +2 -2
  61. package/common/router/pageData.ts +72 -0
  62. package/common/router/register.ts +10 -46
  63. package/common/router/response/page.ts +28 -16
  64. package/docs/assets/unique-domains-chip.png +0 -0
  65. package/docs/dev-sessions.md +8 -4
  66. package/docs/diagnostics.md +77 -11
  67. package/docs/migrate-from-2.1.3.md +388 -0
  68. package/docs/request-tracing.md +42 -9
  69. package/package.json +6 -1
  70. package/scripts/update-codex-agents.ts +2 -2
  71. package/server/app/container/console/index.ts +11 -1
  72. package/server/app/container/trace/index.ts +370 -72
  73. package/server/app/devDiagnostics.ts +1 -1
  74. package/server/app/index.ts +5 -1
  75. package/server/services/auth/index.ts +9 -0
  76. package/server/services/prisma/index.ts +15 -12
  77. package/server/services/router/http/index.ts +1 -1
  78. package/server/services/router/index.ts +105 -23
  79. package/server/services/router/request/api.ts +7 -1
  80. package/server/services/router/request/index.ts +2 -1
  81. package/server/services/router/response/index.ts +8 -28
  82. package/types/global/vendors.d.ts +12 -0
  83. package/types/vendors.d.ts +12 -0
  84. package/common/router/pageSetup.ts +0 -51
@@ -18,6 +18,8 @@ import type { TRequestTrace, TTraceEvent } from './requestTrace';
18
18
  ----------------------------------*/
19
19
 
20
20
  export type TOwnerKind = 'route' | 'controller' | 'command' | 'service' | 'layout' | 'diagnostic';
21
+ export type TOwnerScopeLabel = 'local' | 'generated' | 'connected' | 'framework';
22
+ export type TChainKind = 'route' | 'controller' | 'service' | 'cache' | 'connected' | 'sql';
21
23
 
22
24
  export type TOwnerSource = {
23
25
  filepath: string;
@@ -31,6 +33,8 @@ export type TExplainOwnerMatch = {
31
33
  kind: TOwnerKind;
32
34
  label: string;
33
35
  matchedOn: string[];
36
+ originHint: string;
37
+ scopeLabel: TOwnerScopeLabel;
34
38
  score: number;
35
39
  source: TOwnerSource;
36
40
  };
@@ -55,6 +59,65 @@ export type TTraceAttributionResponse = {
55
59
  sqlQueries: TTraceAttributionItem[];
56
60
  };
57
61
 
62
+ export type TOrientGuidance = {
63
+ agents: string;
64
+ diagnostics: string;
65
+ optimizations: string;
66
+ codingStyle: string;
67
+ areaAgents: string[];
68
+ };
69
+
70
+ export type TOrientConnected = {
71
+ imports: Array<{
72
+ namespace: string;
73
+ clientAccessor: string;
74
+ httpPath: string;
75
+ filepath: string;
76
+ scopeLabel: TOwnerScopeLabel;
77
+ originHint: string;
78
+ }>;
79
+ producers: Array<{
80
+ namespace: string;
81
+ identityIdentifier?: string;
82
+ identityName?: string;
83
+ sourceKind?: string;
84
+ sourceValue?: string;
85
+ urlInternal?: string;
86
+ controllerCount: number;
87
+ cachedContractFilepath?: string;
88
+ typingMode?: string;
89
+ }>;
90
+ };
91
+
92
+ export type TOrientationNextStep = {
93
+ label: string;
94
+ command: string;
95
+ reason: string;
96
+ };
97
+
98
+ export type TOrientResponse = {
99
+ query: string;
100
+ normalizedQuery: string;
101
+ app: {
102
+ appRoot: string;
103
+ repoRoot: string;
104
+ identifier: string;
105
+ routerPort?: number;
106
+ };
107
+ guidance: TOrientGuidance;
108
+ owner: TExplainOwnerResponse;
109
+ connected: TOrientConnected;
110
+ nextSteps: TOrientationNextStep[];
111
+ warnings: string[];
112
+ };
113
+
114
+ export type TDiagnoseChainItem = {
115
+ kind: TChainKind;
116
+ label: string;
117
+ source?: TOwnerSource;
118
+ details: string[];
119
+ };
120
+
58
121
  export type TDiagnoseSuspect = {
59
122
  filepath: string;
60
123
  label: string;
@@ -70,19 +133,23 @@ export type TDiagnoseResponse = {
70
133
  doctor: TDoctorResponse;
71
134
  explainSummary: string[];
72
135
  owner: TExplainOwnerResponse;
136
+ orientation?: Pick<TOrientResponse, 'guidance' | 'connected' | 'nextSteps'>;
137
+ chain?: TDiagnoseChainItem[];
73
138
  query: string;
74
139
  request?: TRequestTrace;
75
140
  serverLogs: TDevConsoleLogsResponse;
76
141
  suspects: TDiagnoseSuspect[];
77
142
  };
78
143
 
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 };
144
+ type TManifestEntry = {
145
+ details: string[];
146
+ kind: TOwnerKind;
147
+ label: string;
148
+ searchTerms: string[];
149
+ source: TOwnerSource;
150
+ originHint: string;
151
+ scopeLabel: TOwnerScopeLabel;
152
+ };
86
153
 
87
154
  type TSuspectAccumulator = {
88
155
  column?: number;
@@ -98,12 +165,102 @@ type TSuspectAccumulator = {
98
165
  ----------------------------------*/
99
166
 
100
167
  const normalizeText = (value: string) => value.trim().replace(/\\/g, '/').toLowerCase();
168
+ const normalizeFilepath = (value: string) => value.replace(/\\/g, '/');
101
169
  const tokenize = (value: string) =>
102
170
  normalizeText(value)
103
171
  .split(/[^a-z0-9/_.-]+/i)
104
172
  .map((token) => token.trim())
105
173
  .filter(Boolean);
106
174
 
175
+ type TNodeFs = {
176
+ existsSync: (filepath: string) => boolean;
177
+ };
178
+
179
+ type TNodePath = {
180
+ dirname: (filepath: string) => string;
181
+ join: (...segments: string[]) => string;
182
+ relative: (from: string, to: string) => string;
183
+ resolve: (...segments: string[]) => string;
184
+ };
185
+
186
+ type TConnectModule = {
187
+ buildConnectResponse: (typeof import('./connect'))['buildConnectResponse'];
188
+ };
189
+
190
+ let cachedNodeFs: TNodeFs | null | undefined;
191
+ let cachedNodePath: TNodePath | null | undefined;
192
+ let cachedConnectModule: TConnectModule | null | undefined;
193
+
194
+ const getNodeFs = () => {
195
+ if (cachedNodeFs !== undefined) return cachedNodeFs || undefined;
196
+
197
+ try {
198
+ cachedNodeFs = (eval('require')('fs') as TNodeFs) || null;
199
+ } catch (_error) {
200
+ cachedNodeFs = null;
201
+ }
202
+
203
+ return cachedNodeFs || undefined;
204
+ };
205
+
206
+ const getNodePath = () => {
207
+ if (cachedNodePath !== undefined) return cachedNodePath || undefined;
208
+
209
+ try {
210
+ cachedNodePath = (eval('require')('path') as TNodePath) || null;
211
+ } catch (_error) {
212
+ cachedNodePath = null;
213
+ }
214
+
215
+ return cachedNodePath || undefined;
216
+ };
217
+
218
+ const getConnectModule = () => {
219
+ if (cachedConnectModule !== undefined) return cachedConnectModule || undefined;
220
+
221
+ try {
222
+ cachedConnectModule = (eval('require')('./connect') as TConnectModule) || null;
223
+ } catch (_error) {
224
+ cachedConnectModule = null;
225
+ }
226
+
227
+ return cachedConnectModule || undefined;
228
+ };
229
+
230
+ const joinPath = (...segments: string[]) => {
231
+ const nodePath = getNodePath();
232
+ if (nodePath) return nodePath.join(...segments);
233
+ return normalizeFilepath(segments.filter(Boolean).join('/')).replace(/\/{2,}/g, '/');
234
+ };
235
+
236
+ const resolvePath = (...segments: string[]) => {
237
+ const nodePath = getNodePath();
238
+ if (nodePath) return nodePath.resolve(...segments);
239
+ return normalizeFilepath(segments.filter(Boolean).join('/')).replace(/\/{2,}/g, '/');
240
+ };
241
+
242
+ const dirnamePath = (filepath: string) => {
243
+ const nodePath = getNodePath();
244
+ if (nodePath) return nodePath.dirname(filepath);
245
+
246
+ const normalized = normalizeFilepath(filepath).replace(/\/+$/, '');
247
+ const slashIndex = normalized.lastIndexOf('/');
248
+ if (slashIndex <= 0) return slashIndex === 0 ? '/' : '.';
249
+ return normalized.slice(0, slashIndex);
250
+ };
251
+
252
+ const relativePath = (from: string, to: string) => {
253
+ const nodePath = getNodePath();
254
+ if (nodePath) return nodePath.relative(from, to);
255
+
256
+ const normalizedFrom = normalizeFilepath(from).replace(/\/+$/, '');
257
+ const normalizedTo = normalizeFilepath(to);
258
+ if (normalizedTo.startsWith(`${normalizedFrom}/`)) return normalizedTo.slice(normalizedFrom.length + 1);
259
+ return normalizedTo;
260
+ };
261
+
262
+ const fileExists = (filepath: string) => getNodeFs()?.existsSync(filepath) === true;
263
+
107
264
  /*----------------------------------
108
265
  - HELPERS
109
266
  ----------------------------------*/
@@ -115,6 +272,60 @@ const toSource = (filepath: string, sourceLocation?: TProteumManifestSourceLocat
115
272
  ...(scope ? { scope } : {}),
116
273
  });
117
274
 
275
+ const isGeneratedFilepath = (manifest: TProteumManifest, filepath: string) => {
276
+ const normalizedFilepath = normalizeFilepath(filepath);
277
+ const normalizedAppRoot = normalizeFilepath(manifest.app.root);
278
+
279
+ return (
280
+ normalizedFilepath.includes('/.proteum/') ||
281
+ normalizedFilepath === `${normalizedAppRoot}/proteum.connected.json` ||
282
+ normalizedFilepath.endsWith('/proteum.connected.json')
283
+ );
284
+ };
285
+
286
+ const resolveScopeLabel = ({
287
+ filepath,
288
+ manifest,
289
+ scope,
290
+ }: {
291
+ filepath: string;
292
+ manifest: TProteumManifest;
293
+ scope?: TProteumManifestScope;
294
+ }): TOwnerScopeLabel => {
295
+ if (scope === 'connected') return 'connected';
296
+ if (scope === 'framework') return 'framework';
297
+ if (isGeneratedFilepath(manifest, filepath)) return 'generated';
298
+ return 'local';
299
+ };
300
+
301
+ const resolveOriginHint = ({
302
+ manifest,
303
+ filepath,
304
+ scopeLabel,
305
+ fallback,
306
+ connectedNamespace,
307
+ }: {
308
+ manifest: TProteumManifest;
309
+ filepath: string;
310
+ scopeLabel: TOwnerScopeLabel;
311
+ fallback: string;
312
+ connectedNamespace?: string;
313
+ }) => {
314
+ if (scopeLabel === 'connected') {
315
+ return connectedNamespace ? `connected boundary import from ${connectedNamespace}` : 'connected boundary import';
316
+ }
317
+
318
+ if (scopeLabel === 'framework') {
319
+ const relativeFrameworkPath = normalizeFilepath(relativePath(manifest.app.coreRoot, filepath));
320
+ return relativeFrameworkPath && relativeFrameworkPath !== '..'
321
+ ? `framework-owned source ${relativeFrameworkPath}`
322
+ : 'framework-owned source';
323
+ }
324
+
325
+ if (scopeLabel === 'generated') return 'generated Proteum artifact';
326
+ return fallback;
327
+ };
328
+
118
329
  const pushTerm = (terms: Set<string>, value?: string) => {
119
330
  if (!value) return;
120
331
 
@@ -128,8 +339,10 @@ const pushTerm = (terms: Set<string>, value?: string) => {
128
339
  for (const token of tokenize(value)) terms.add(token);
129
340
  };
130
341
 
131
- const createRouteEntry = (route: TProteumManifestRoute): TManifestEntry => {
342
+ const createRouteEntry = (manifest: TProteumManifest, route: TProteumManifestRoute): TManifestEntry => {
132
343
  const terms = new Set<string>();
344
+ const filepath = route.filepath;
345
+ const scopeLabel = resolveScopeLabel({ filepath, manifest, scope: route.scope });
133
346
 
134
347
  pushTerm(terms, route.filepath);
135
348
  pushTerm(terms, route.path);
@@ -145,17 +358,28 @@ const createRouteEntry = (route: TProteumManifestRoute): TManifestEntry => {
145
358
  `${route.kind} ${route.methodName}`,
146
359
  ...(route.path ? [`path=${route.path}`] : []),
147
360
  ...(route.chunkId ? [`chunk=${route.chunkId}`] : []),
148
- `setup=${route.hasSetup ? 'yes' : 'no'}`,
361
+ `data=${route.hasData ? 'yes' : 'no'}`,
149
362
  ],
150
363
  kind: 'route',
151
364
  label: route.path || route.pathRaw || route.chunkId || route.filepath,
152
365
  searchTerms: [...terms],
153
366
  source: toSource(route.filepath, route.sourceLocation, route.scope),
367
+ originHint: resolveOriginHint({
368
+ manifest,
369
+ filepath,
370
+ scopeLabel,
371
+ fallback: route.chunkId ? `local route chunk ${route.chunkId}` : 'local route source',
372
+ }),
373
+ scopeLabel,
154
374
  };
155
375
  };
156
376
 
157
- const createControllerEntry = (controller: TProteumManifestController): TManifestEntry => {
377
+ const createControllerEntry = (manifest: TProteumManifest, controller: TProteumManifestController): TManifestEntry => {
158
378
  const terms = new Set<string>();
379
+ const filepath = controller.filepath;
380
+ const scopeLabel = controller.connectedProjectNamespace
381
+ ? 'connected'
382
+ : resolveScopeLabel({ filepath, manifest, scope: controller.scope });
159
383
 
160
384
  pushTerm(terms, controller.filepath);
161
385
  pushTerm(terms, controller.className);
@@ -163,6 +387,7 @@ const createControllerEntry = (controller: TProteumManifestController): TManifes
163
387
  pushTerm(terms, controller.routePath);
164
388
  pushTerm(terms, controller.httpPath);
165
389
  pushTerm(terms, controller.clientAccessor);
390
+ pushTerm(terms, controller.connectedProjectNamespace);
166
391
 
167
392
  return {
168
393
  details: [
@@ -170,16 +395,27 @@ const createControllerEntry = (controller: TProteumManifestController): TManifes
170
395
  `method=${controller.methodName}`,
171
396
  `http=${controller.httpPath}`,
172
397
  `client=${controller.clientAccessor}`,
398
+ ...(controller.connectedProjectNamespace ? [`connected=${controller.connectedProjectNamespace}`] : []),
173
399
  ],
174
400
  kind: 'controller',
175
401
  label: controller.httpPath,
176
402
  searchTerms: [...terms],
177
403
  source: toSource(controller.filepath, controller.sourceLocation, controller.scope),
404
+ originHint: resolveOriginHint({
405
+ manifest,
406
+ filepath,
407
+ scopeLabel,
408
+ fallback: `local controller ${controller.className}`,
409
+ connectedNamespace: controller.connectedProjectNamespace,
410
+ }),
411
+ scopeLabel,
178
412
  };
179
413
  };
180
414
 
181
- const createCommandEntry = (command: TProteumManifestCommand): TManifestEntry => {
415
+ const createCommandEntry = (manifest: TProteumManifest, command: TProteumManifestCommand): TManifestEntry => {
182
416
  const terms = new Set<string>();
417
+ const filepath = command.filepath;
418
+ const scopeLabel = resolveScopeLabel({ filepath, manifest, scope: command.scope });
183
419
 
184
420
  pushTerm(terms, command.filepath);
185
421
  pushTerm(terms, command.className);
@@ -192,12 +428,20 @@ const createCommandEntry = (command: TProteumManifestCommand): TManifestEntry =>
192
428
  label: command.path,
193
429
  searchTerms: [...terms],
194
430
  source: toSource(command.filepath, command.sourceLocation, command.scope),
431
+ originHint: resolveOriginHint({
432
+ manifest,
433
+ filepath,
434
+ scopeLabel,
435
+ fallback: `local command ${command.className}`,
436
+ }),
437
+ scopeLabel,
195
438
  };
196
439
  };
197
440
 
198
- const createServiceEntry = (service: TProteumManifestService): TManifestEntry => {
441
+ const createServiceEntry = (manifest: TProteumManifest, service: TProteumManifestService): TManifestEntry => {
199
442
  const terms = new Set<string>();
200
- const sourceFilepath = service.sourceFilepath || '';
443
+ const sourceFilepath = service.sourceFilepath || service.importPath || service.registeredName;
444
+ const scopeLabel = resolveScopeLabel({ filepath: sourceFilepath, manifest, scope: service.scope });
201
445
 
202
446
  pushTerm(terms, service.registeredName);
203
447
  pushTerm(terms, service.className);
@@ -217,12 +461,21 @@ const createServiceEntry = (service: TProteumManifestService): TManifestEntry =>
217
461
  kind: 'service',
218
462
  label: service.registeredName,
219
463
  searchTerms: [...terms],
220
- source: toSource(sourceFilepath || service.importPath || service.registeredName, undefined, service.scope),
464
+ source: toSource(sourceFilepath, undefined, service.scope),
465
+ originHint: resolveOriginHint({
466
+ manifest,
467
+ filepath: sourceFilepath,
468
+ scopeLabel,
469
+ fallback: `service registration ${service.registeredName}`,
470
+ }),
471
+ scopeLabel,
221
472
  };
222
473
  };
223
474
 
224
- const createLayoutEntry = (layout: TProteumManifestLayout): TManifestEntry => {
475
+ const createLayoutEntry = (manifest: TProteumManifest, layout: TProteumManifestLayout): TManifestEntry => {
225
476
  const terms = new Set<string>();
477
+ const filepath = layout.filepath;
478
+ const scopeLabel = resolveScopeLabel({ filepath, manifest, scope: layout.scope });
226
479
 
227
480
  pushTerm(terms, layout.filepath);
228
481
  pushTerm(terms, layout.chunkId);
@@ -234,35 +487,57 @@ const createLayoutEntry = (layout: TProteumManifestLayout): TManifestEntry => {
234
487
  label: layout.chunkId || layout.filepath,
235
488
  searchTerms: [...terms],
236
489
  source: toSource(layout.filepath, undefined, layout.scope),
490
+ originHint: resolveOriginHint({
491
+ manifest,
492
+ filepath,
493
+ scopeLabel,
494
+ fallback: layout.chunkId ? `layout chunk ${layout.chunkId}` : 'layout source',
495
+ }),
496
+ scopeLabel,
237
497
  };
238
498
  };
239
499
 
240
- const createDiagnosticEntry = (diagnostic: TProteumManifestDiagnostic): TManifestEntry => {
500
+ const createDiagnosticEntry = (manifest: TProteumManifest, diagnostic: TProteumManifestDiagnostic): TManifestEntry => {
241
501
  const terms = new Set<string>();
502
+ const filepath = diagnostic.filepath;
503
+ const scopeLabel = resolveScopeLabel({ filepath, manifest });
242
504
 
243
505
  pushTerm(terms, diagnostic.code);
244
506
  pushTerm(terms, diagnostic.message);
245
507
  pushTerm(terms, diagnostic.filepath);
508
+ pushTerm(terms, diagnostic.fixHint);
246
509
  for (const related of diagnostic.relatedFilepaths || []) pushTerm(terms, related);
247
510
 
248
511
  return {
249
- details: [`[${diagnostic.level}]`, diagnostic.message, ...(diagnostic.relatedFilepaths || []).map((filepath) => `related=${filepath}`)],
512
+ details: [
513
+ `[${diagnostic.level}]`,
514
+ diagnostic.message,
515
+ ...(diagnostic.fixHint ? [`fix=${diagnostic.fixHint}`] : []),
516
+ ...(diagnostic.relatedFilepaths || []).map((relatedFilepath) => `related=${relatedFilepath}`),
517
+ ],
250
518
  kind: 'diagnostic',
251
519
  label: diagnostic.code,
252
520
  searchTerms: [...terms],
253
521
  source: toSource(diagnostic.filepath, diagnostic.sourceLocation),
522
+ originHint: resolveOriginHint({
523
+ manifest,
524
+ filepath,
525
+ scopeLabel,
526
+ fallback: 'local diagnostic source',
527
+ }),
528
+ scopeLabel,
254
529
  };
255
530
  };
256
531
 
257
532
  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),
533
+ ...manifest.routes.client.map((route) => createRouteEntry(manifest, route)),
534
+ ...manifest.routes.server.map((route) => createRouteEntry(manifest, route)),
535
+ ...manifest.controllers.map((controller) => createControllerEntry(manifest, controller)),
536
+ ...manifest.commands.map((command) => createCommandEntry(manifest, command)),
537
+ ...manifest.services.app.map((service) => createServiceEntry(manifest, service)),
538
+ ...manifest.services.routerPlugins.map((service) => createServiceEntry(manifest, service)),
539
+ ...manifest.layouts.map((layout) => createLayoutEntry(manifest, layout)),
540
+ ...manifest.diagnostics.map((diagnostic) => createDiagnosticEntry(manifest, diagnostic)),
266
541
  ];
267
542
 
268
543
  const scoreOwnerMatch = (query: string, entry: TManifestEntry) => {
@@ -299,6 +574,9 @@ const scoreOwnerMatch = (query: string, entry: TManifestEntry) => {
299
574
 
300
575
  if ((entry.kind === 'controller' && entry.label === query) || (entry.kind === 'route' && entry.label === query)) score += 30;
301
576
 
577
+ if (entry.scopeLabel === 'connected' && tokenize(query).some((token) => token === 'connected')) score += 6;
578
+ if (entry.scopeLabel === 'generated' && normalizeText(query).includes('.proteum')) score += 8;
579
+
302
580
  return {
303
581
  matchedOn: [...new Set(matchedOn)],
304
582
  score,
@@ -310,6 +588,8 @@ const toOwnerMatch = (entry: TManifestEntry, score: number, matchedOn: string[])
310
588
  kind: entry.kind,
311
589
  label: entry.label,
312
590
  matchedOn,
591
+ originHint: entry.originHint,
592
+ scopeLabel: entry.scopeLabel,
313
593
  score,
314
594
  source: entry.source,
315
595
  });
@@ -355,6 +635,35 @@ const getEventQuery = (event: TTraceEvent) => {
355
635
  return '';
356
636
  };
357
637
 
638
+ const readEventString = (event: TTraceEvent | undefined, key: string) => {
639
+ if (!event) return undefined;
640
+ const value = event.details[key];
641
+ return typeof value === 'string' ? value : undefined;
642
+ };
643
+
644
+ const readEventNumber = (event: TTraceEvent | undefined, key: string) => {
645
+ if (!event) return undefined;
646
+ const value = event.details[key];
647
+ return typeof value === 'number' ? value : undefined;
648
+ };
649
+
650
+ const readEventSource = (event: TTraceEvent): TOwnerSource | undefined => {
651
+ const source = event.details.source;
652
+ if (typeof source !== 'object' || !source || !('entries' in source)) return undefined;
653
+
654
+ const filepath = source.entries.filepath;
655
+ if (typeof filepath !== 'string' || !filepath) return undefined;
656
+
657
+ const line = source.entries.line;
658
+ const column = source.entries.column;
659
+
660
+ return {
661
+ filepath,
662
+ ...(typeof line === 'number' && line > 0 ? { line } : {}),
663
+ ...(typeof column === 'number' && column > 0 ? { column } : {}),
664
+ };
665
+ };
666
+
358
667
  const addSuspect = (
359
668
  suspects: Map<string, TSuspectAccumulator>,
360
669
  owner: TExplainOwnerMatch | undefined,
@@ -382,6 +691,450 @@ const addSuspect = (
382
691
  });
383
692
  };
384
693
 
694
+ const findRepoRoot = (startPath: string) => {
695
+ let currentPath = resolvePath(startPath);
696
+
697
+ while (true) {
698
+ const gitDir = joinPath(currentPath, '.git');
699
+ if (fileExists(gitDir)) return currentPath;
700
+
701
+ const parentPath = dirnamePath(currentPath);
702
+ if (parentPath === currentPath) return resolvePath(startPath);
703
+ currentPath = parentPath;
704
+ }
705
+ };
706
+
707
+ const resolveGuidanceFile = ({
708
+ appRoot,
709
+ fallbackFilepath,
710
+ relativePath,
711
+ }: {
712
+ appRoot: string;
713
+ fallbackFilepath: string;
714
+ relativePath: string;
715
+ }) => {
716
+ const localFilepath = joinPath(appRoot, relativePath);
717
+ if (fileExists(localFilepath)) return { filepath: localFilepath, warning: undefined as string | undefined };
718
+
719
+ return {
720
+ filepath: fallbackFilepath,
721
+ warning: `Missing ${relativePath} in ${appRoot}; using ${fallbackFilepath}.`,
722
+ };
723
+ };
724
+
725
+ const resolveAreaAgents = ({
726
+ appRoot,
727
+ coreRoot,
728
+ ownerFilepath,
729
+ }: {
730
+ appRoot: string;
731
+ coreRoot: string;
732
+ ownerFilepath?: string;
733
+ }) => {
734
+ if (!ownerFilepath) return [];
735
+
736
+ const fallbackRoot = joinPath(coreRoot, 'agents', 'project');
737
+ const normalizedOwner = normalizeFilepath(resolvePath(ownerFilepath));
738
+ const normalizedAppRoot = normalizeFilepath(resolvePath(appRoot));
739
+ const relativeOwner = normalizedOwner.startsWith(`${normalizedAppRoot}/`)
740
+ ? normalizeFilepath(relativePath(appRoot, ownerFilepath))
741
+ : undefined;
742
+
743
+ if (!relativeOwner) return [];
744
+
745
+ const relativeDir = normalizeFilepath(dirnamePath(relativeOwner));
746
+ if (relativeDir === '.' || relativeDir === '') return [];
747
+
748
+ const segments = relativeDir.split('/').filter(Boolean);
749
+ const areaAgents: string[] = [];
750
+ for (let index = 0; index < segments.length; index++) {
751
+ const areaRelativePath = joinPath(...segments.slice(0, index + 1), 'AGENTS.md');
752
+ const localFilepath = joinPath(appRoot, areaRelativePath);
753
+ const fallbackFilepath = joinPath(fallbackRoot, areaRelativePath);
754
+
755
+ if (fileExists(localFilepath)) areaAgents.push(localFilepath);
756
+ else if (fileExists(fallbackFilepath)) areaAgents.push(fallbackFilepath);
757
+ }
758
+
759
+ return [...new Set(areaAgents)];
760
+ };
761
+
762
+ const resolveGuidance = ({
763
+ manifest,
764
+ ownerFilepath,
765
+ }: {
766
+ manifest: TProteumManifest;
767
+ ownerFilepath?: string;
768
+ }) => {
769
+ const fallbackRoot = joinPath(manifest.app.coreRoot, 'agents', 'project');
770
+ const warnings: string[] = [];
771
+ const agents = resolveGuidanceFile({
772
+ appRoot: manifest.app.root,
773
+ fallbackFilepath: joinPath(fallbackRoot, 'AGENTS.md'),
774
+ relativePath: 'AGENTS.md',
775
+ });
776
+ const diagnostics = resolveGuidanceFile({
777
+ appRoot: manifest.app.root,
778
+ fallbackFilepath: joinPath(fallbackRoot, 'diagnostics.md'),
779
+ relativePath: 'diagnostics.md',
780
+ });
781
+ const optimizations = resolveGuidanceFile({
782
+ appRoot: manifest.app.root,
783
+ fallbackFilepath: joinPath(fallbackRoot, 'optimizations.md'),
784
+ relativePath: 'optimizations.md',
785
+ });
786
+ const codingStyle = resolveGuidanceFile({
787
+ appRoot: manifest.app.root,
788
+ fallbackFilepath: joinPath(fallbackRoot, 'CODING_STYLE.md'),
789
+ relativePath: 'CODING_STYLE.md',
790
+ });
791
+
792
+ for (const warning of [agents.warning, diagnostics.warning, optimizations.warning, codingStyle.warning]) {
793
+ if (warning) warnings.push(warning);
794
+ }
795
+
796
+ return {
797
+ guidance: {
798
+ agents: agents.filepath,
799
+ diagnostics: diagnostics.filepath,
800
+ optimizations: optimizations.filepath,
801
+ codingStyle: codingStyle.filepath,
802
+ areaAgents: resolveAreaAgents({
803
+ appRoot: manifest.app.root,
804
+ coreRoot: manifest.app.coreRoot,
805
+ ownerFilepath,
806
+ }),
807
+ } satisfies TOrientGuidance,
808
+ warnings,
809
+ };
810
+ };
811
+
812
+ const getConnectedNamespaceForOwner = (manifest: TProteumManifest, owner?: TExplainOwnerMatch) => {
813
+ if (!owner || owner.kind !== 'controller') return undefined;
814
+
815
+ const controller = manifest.controllers.find(
816
+ (candidate) =>
817
+ candidate.httpPath === owner.label &&
818
+ candidate.filepath === owner.source.filepath &&
819
+ candidate.sourceLocation.line === (owner.source.line || candidate.sourceLocation.line),
820
+ );
821
+
822
+ return controller?.connectedProjectNamespace;
823
+ };
824
+
825
+ const buildConnectedSummary = ({
826
+ manifest,
827
+ owner,
828
+ query,
829
+ }: {
830
+ manifest: TProteumManifest;
831
+ owner: TExplainOwnerResponse;
832
+ query: string;
833
+ }) => {
834
+ const tokens = tokenize(query);
835
+ const topOwner = owner.matches[0];
836
+ const ownerNamespace = getConnectedNamespaceForOwner(manifest, topOwner);
837
+ const queryLikelyCrossesBoundary =
838
+ owner.matches.some((match) => match.scopeLabel === 'connected') ||
839
+ manifest.connectedProjects.some((project) =>
840
+ [project.namespace, project.identityIdentifier, project.identityName].some(
841
+ (value) => typeof value === 'string' && tokens.some((token) => normalizeText(value).includes(token)),
842
+ ),
843
+ );
844
+
845
+ const imports = manifest.controllers
846
+ .filter((controller) => controller.connectedProjectNamespace)
847
+ .map((controller) => {
848
+ const namespace = controller.connectedProjectNamespace as string;
849
+ const score =
850
+ (ownerNamespace === namespace ? 100 : 0) +
851
+ (topOwner?.source.filepath === controller.filepath ? 40 : 0) +
852
+ (tokens.some((token) => normalizeText(namespace).includes(token)) ? 20 : 0) +
853
+ (tokens.some((token) => normalizeText(controller.clientAccessor).includes(token)) ? 18 : 0) +
854
+ (tokens.some((token) => normalizeText(controller.httpPath).includes(token)) ? 18 : 0);
855
+
856
+ return {
857
+ namespace,
858
+ controller,
859
+ score,
860
+ };
861
+ })
862
+ .filter(({ score }) => score > 0 || queryLikelyCrossesBoundary)
863
+ .sort((left, right) => right.score - left.score || left.controller.clientAccessor.localeCompare(right.controller.clientAccessor))
864
+ .slice(0, queryLikelyCrossesBoundary ? 6 : 4)
865
+ .map(({ namespace, controller }) => ({
866
+ namespace,
867
+ clientAccessor: controller.clientAccessor,
868
+ httpPath: controller.httpPath,
869
+ filepath: controller.filepath,
870
+ scopeLabel: 'connected' as const,
871
+ originHint: `connected boundary import from ${namespace}`,
872
+ }));
873
+
874
+ const producerNamespaceSet = new Set<string>([ownerNamespace, ...imports.map((item) => item.namespace)].filter(Boolean) as string[]);
875
+ const producers = manifest.connectedProjects
876
+ .map((project) => {
877
+ const score =
878
+ (producerNamespaceSet.has(project.namespace) ? 100 : 0) +
879
+ (tokens.some((token) => normalizeText(project.namespace).includes(token)) ? 20 : 0) +
880
+ (tokens.some((token) => normalizeText(project.identityIdentifier || '').includes(token)) ? 18 : 0) +
881
+ (tokens.some((token) => normalizeText(project.identityName || '').includes(token)) ? 18 : 0);
882
+
883
+ return {
884
+ project,
885
+ score,
886
+ };
887
+ })
888
+ .filter(({ score }) => score > 0 || (queryLikelyCrossesBoundary && manifest.connectedProjects.length > 0))
889
+ .sort((left, right) => right.score - left.score || left.project.namespace.localeCompare(right.project.namespace))
890
+ .slice(0, queryLikelyCrossesBoundary ? 4 : 2)
891
+ .map(({ project }) => ({
892
+ namespace: project.namespace,
893
+ identityIdentifier: project.identityIdentifier,
894
+ identityName: project.identityName,
895
+ sourceKind: project.sourceKind,
896
+ sourceValue: project.sourceValue,
897
+ urlInternal: project.urlInternal,
898
+ controllerCount: project.controllerCount,
899
+ cachedContractFilepath: project.cachedContractFilepath,
900
+ typingMode: project.typingMode,
901
+ }));
902
+
903
+ return {
904
+ imports,
905
+ producers,
906
+ } satisfies TOrientConnected;
907
+ };
908
+
909
+ const quoteShellArgument = (value: string) => JSON.stringify(value);
910
+
911
+ const resolveRequestTarget = (owner: TExplainOwnerResponse, query: string) => {
912
+ if (query.startsWith('/')) return query;
913
+
914
+ const topOwner = owner.matches[0];
915
+ if (!topOwner) return undefined;
916
+ if ((topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')) return topOwner.label;
917
+
918
+ return undefined;
919
+ };
920
+
921
+ const buildNextSteps = ({
922
+ connected,
923
+ owner,
924
+ query,
925
+ }: {
926
+ connected: TOrientConnected;
927
+ owner: TExplainOwnerResponse;
928
+ query: string;
929
+ }) => {
930
+ const requestTarget = resolveRequestTarget(owner, query);
931
+ const topOwner = owner.matches[0];
932
+ const steps: TOrientationNextStep[] = [
933
+ {
934
+ label: 'Verify Owner',
935
+ command: `proteum verify owner ${quoteShellArgument(query)}`,
936
+ reason: 'Separate owner-scoped blocking findings from unrelated global diagnostics.',
937
+ },
938
+ ];
939
+
940
+ if (topOwner?.kind === 'command') {
941
+ steps.push({
942
+ label: 'Run Command',
943
+ command: `proteum command ${quoteShellArgument(topOwner.label)}`,
944
+ reason: 'Exercise the smallest trustworthy runtime surface for the matched command owner.',
945
+ });
946
+ } else if (requestTarget) {
947
+ steps.push({
948
+ label: 'Diagnose Request',
949
+ command: `proteum diagnose ${quoteShellArgument(requestTarget)} --hit ${quoteShellArgument(requestTarget)}`,
950
+ reason: 'Hit the real runtime once and capture owner lookup, trace data, and server diagnostics together.',
951
+ });
952
+ } else {
953
+ steps.push({
954
+ label: 'Explain Owner',
955
+ command: `proteum explain owner ${quoteShellArgument(query)}`,
956
+ reason: 'Inspect the exact manifest owner match before reading more source.',
957
+ });
958
+ }
959
+
960
+ if (connected.imports.length > 0 || connected.producers.length > 0) {
961
+ steps.push({
962
+ label: 'Inspect Connected',
963
+ command: 'proteum connect --controllers',
964
+ reason: 'Confirm imported controllers, contract cache state, and runtime internal URLs for the connected boundary.',
965
+ });
966
+ } else if (requestTarget) {
967
+ steps.push({
968
+ label: 'Inspect Perf',
969
+ command: `proteum perf request ${quoteShellArgument(requestTarget)}`,
970
+ reason: 'Summarize SQL, render, cache, and fetcher cost for the same request surface.',
971
+ });
972
+ } else {
973
+ steps.push({
974
+ label: 'Check Contracts',
975
+ command: 'proteum doctor --contracts',
976
+ reason: 'Confirm the framework-owned source and generated artifact contract before broader checks.',
977
+ });
978
+ }
979
+
980
+ return steps
981
+ .filter((step, index, list) => list.findIndex((candidate) => candidate.command === step.command) === index)
982
+ .slice(0, 3);
983
+ };
984
+
985
+ const getServiceSource = (manifest: TProteumManifest, label: string): TOwnerSource | undefined => {
986
+ const service = [...manifest.services.app, ...manifest.services.routerPlugins].find((candidate) => candidate.registeredName === label);
987
+ if (!service || !service.sourceFilepath) return undefined;
988
+
989
+ return {
990
+ filepath: service.sourceFilepath,
991
+ ...(service.scope ? { scope: service.scope } : {}),
992
+ };
993
+ };
994
+
995
+ const pushChainItem = (items: TDiagnoseChainItem[], item: TDiagnoseChainItem | undefined) => {
996
+ if (!item) return;
997
+
998
+ const key = `${item.kind}:${item.label}:${item.source?.filepath || ''}:${item.source?.line || 0}:${item.source?.column || 0}`;
999
+ if (items.some((candidate) => `${candidate.kind}:${candidate.label}:${candidate.source?.filepath || ''}:${candidate.source?.line || 0}:${candidate.source?.column || 0}` === key)) return;
1000
+
1001
+ items.push(item);
1002
+ };
1003
+
1004
+ export const buildRequestChain = ({
1005
+ manifest,
1006
+ owner,
1007
+ request,
1008
+ }: {
1009
+ manifest: TProteumManifest;
1010
+ owner: TExplainOwnerResponse;
1011
+ request?: TRequestTrace;
1012
+ }) => {
1013
+ if (!request) return undefined;
1014
+
1015
+ const chain: TDiagnoseChainItem[] = [];
1016
+ const topOwner = owner.matches[0];
1017
+
1018
+ if (topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller')) {
1019
+ pushChainItem(chain, {
1020
+ kind: topOwner.kind,
1021
+ label: topOwner.label,
1022
+ source: topOwner.source,
1023
+ details: [`scope=${topOwner.scopeLabel}`, `origin=${topOwner.originHint}`],
1024
+ });
1025
+ }
1026
+
1027
+ const routeEvent = request.events.find((event) => event.type === 'resolve.route-match');
1028
+ const routeLabel = readEventString(routeEvent as TTraceEvent, 'routePath');
1029
+ if (!topOwner || topOwner.kind !== 'route') {
1030
+ pushChainItem(
1031
+ chain,
1032
+ routeEvent && routeLabel
1033
+ ? {
1034
+ kind: 'route',
1035
+ label: routeLabel,
1036
+ source: readEventSource(routeEvent),
1037
+ details: [
1038
+ ...(readEventString(routeEvent, 'method') ? [`method=${readEventString(routeEvent, 'method')}`] : []),
1039
+ ...(readEventString(routeEvent, 'accept') ? [`accept=${readEventString(routeEvent, 'accept')}`] : []),
1040
+ ],
1041
+ }
1042
+ : undefined,
1043
+ );
1044
+ }
1045
+
1046
+ const controllerEvent = request.events.find((event) => event.type === 'resolve.controller-route' || event.type === 'controller.start');
1047
+ const controllerLabel = readEventString(controllerEvent as TTraceEvent, 'target') || request.path;
1048
+ if ((!topOwner || topOwner.kind !== 'controller') && request.path.startsWith('/api')) {
1049
+ pushChainItem(
1050
+ chain,
1051
+ controllerEvent
1052
+ ? {
1053
+ kind: 'controller',
1054
+ label: controllerLabel,
1055
+ source: readEventSource(controllerEvent),
1056
+ details: [
1057
+ ...(readEventString(controllerEvent, 'filepath') ? [`source=${readEventString(controllerEvent, 'filepath')}`] : []),
1058
+ ],
1059
+ }
1060
+ : undefined,
1061
+ );
1062
+ }
1063
+
1064
+ const serviceLabels = [
1065
+ ...request.calls.flatMap((call) => (call.serviceLabel ? [call.serviceLabel] : [])),
1066
+ ...request.sqlQueries.flatMap((query) => (query.serviceLabel ? [query.serviceLabel] : [])),
1067
+ ].filter(Boolean);
1068
+
1069
+ for (const serviceLabel of [...new Set(serviceLabels)].slice(0, 6)) {
1070
+ pushChainItem(chain, {
1071
+ kind: 'service',
1072
+ label: serviceLabel,
1073
+ source: getServiceSource(manifest, serviceLabel),
1074
+ details: ['service method observed in traced call or SQL stack'],
1075
+ });
1076
+ }
1077
+
1078
+ const cacheHitEvent = request.events.find((event) => event.type === 'cache.hit');
1079
+ if (cacheHitEvent) {
1080
+ pushChainItem(chain, {
1081
+ kind: 'cache',
1082
+ label: `html-cache ${readEventString(cacheHitEvent, 'cachePhase') || 'hit'}`,
1083
+ details: [
1084
+ ...(readEventString(cacheHitEvent, 'cacheKey') ? [`key=${readEventString(cacheHitEvent, 'cacheKey')}`] : []),
1085
+ ],
1086
+ });
1087
+ }
1088
+
1089
+ const cacheWriteEvents = request.events.filter((event) => event.type === 'cache.write');
1090
+ if (!cacheHitEvent && cacheWriteEvents.length > 0) {
1091
+ for (const cacheWriteEvent of cacheWriteEvents.slice(0, 3)) {
1092
+ pushChainItem(chain, {
1093
+ kind: 'cache',
1094
+ label: `html-cache ${readEventString(cacheWriteEvent, 'cachePhase') || 'write'}`,
1095
+ details: [
1096
+ ...(readEventString(cacheWriteEvent, 'cacheKey') ? [`key=${readEventString(cacheWriteEvent, 'cacheKey')}`] : []),
1097
+ ],
1098
+ });
1099
+ }
1100
+ }
1101
+
1102
+ const connectedNamespaces = [
1103
+ ...request.calls.flatMap((call) => (call.connectedProjectNamespace ? [call.connectedProjectNamespace] : [])),
1104
+ ...request.sqlQueries.flatMap((query) => (query.connectedNamespace ? [query.connectedNamespace] : [])),
1105
+ ].filter(Boolean);
1106
+
1107
+ for (const namespace of [...new Set(connectedNamespaces)].slice(0, 4)) {
1108
+ const project = manifest.connectedProjects.find((candidate) => candidate.namespace === namespace);
1109
+ pushChainItem(chain, {
1110
+ kind: 'connected',
1111
+ label: namespace,
1112
+ details: [
1113
+ ...(project?.identityIdentifier ? [`identifier=${project.identityIdentifier}`] : []),
1114
+ ...(project?.urlInternal ? [`urlInternal=${project.urlInternal}`] : []),
1115
+ ...(project?.sourceKind ? [`source=${project.sourceKind}`] : []),
1116
+ ],
1117
+ });
1118
+ }
1119
+
1120
+ const sqlFingerprints = request.sqlQueries
1121
+ .flatMap((query) => (query.fingerprint ? [query.fingerprint] : []))
1122
+ .filter(Boolean);
1123
+ for (const fingerprint of [...new Set(sqlFingerprints)].slice(0, 8)) {
1124
+ const matchingQuery = request.sqlQueries.find((query) => query.fingerprint === fingerprint);
1125
+ pushChainItem(chain, {
1126
+ kind: 'sql',
1127
+ label: fingerprint,
1128
+ details: [
1129
+ ...(matchingQuery?.operation ? [`operation=${matchingQuery.operation}`] : []),
1130
+ ...(matchingQuery?.model ? [`model=${matchingQuery.model}`] : []),
1131
+ ],
1132
+ });
1133
+ }
1134
+
1135
+ return chain.length > 0 ? chain : undefined;
1136
+ };
1137
+
385
1138
  /*----------------------------------
386
1139
  - PUBLIC API
387
1140
  ----------------------------------*/
@@ -402,6 +1155,37 @@ export const explainOwner = (manifest: TProteumManifest, query: string): TExplai
402
1155
  return { matches, normalizedQuery, query };
403
1156
  };
404
1157
 
1158
+ export const buildOrientationResponse = (manifest: TProteumManifest, query: string): TOrientResponse => {
1159
+ const owner = explainOwner(manifest, query);
1160
+ const ownerFilepath = owner.matches[0]?.source.filepath;
1161
+ const guidanceResolution = resolveGuidance({ manifest, ownerFilepath });
1162
+ const connected = buildConnectedSummary({ manifest, owner, query });
1163
+ const nextSteps = buildNextSteps({ connected, owner, query });
1164
+ const connectResponse = getConnectModule()?.buildConnectResponse(manifest, { includeControllers: true });
1165
+ const warnings = [...guidanceResolution.warnings];
1166
+
1167
+ if (owner.matches.length === 0) warnings.push(`No manifest owner matched "${query}".`);
1168
+ if (connectResponse?.diagnostics.some((diagnostic) => diagnostic.level === 'error')) {
1169
+ warnings.push('Connected project diagnostics contain errors; inspect `proteum connect --controllers` before broader verification.');
1170
+ }
1171
+
1172
+ return {
1173
+ query,
1174
+ normalizedQuery: owner.normalizedQuery,
1175
+ app: {
1176
+ appRoot: manifest.app.root,
1177
+ repoRoot: findRepoRoot(manifest.app.root),
1178
+ identifier: manifest.app.identity.identifier,
1179
+ ...(typeof manifest.env?.resolved?.routerPort === 'number' ? { routerPort: manifest.env.resolved.routerPort } : {}),
1180
+ },
1181
+ guidance: guidanceResolution.guidance,
1182
+ owner,
1183
+ connected,
1184
+ nextSteps,
1185
+ warnings,
1186
+ };
1187
+ };
1188
+
405
1189
  export const buildTraceAttribution = (manifest: TProteumManifest, request?: TRequestTrace): TTraceAttributionResponse | undefined => {
406
1190
  if (!request) return undefined;
407
1191
 
@@ -412,22 +1196,22 @@ export const buildTraceAttribution = (manifest: TProteumManifest, request?: TReq
412
1196
 
413
1197
  return {
414
1198
  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
- }),
1199
+ const query = call.ownerFilepath || call.path || call.label;
1200
+ return query
1201
+ ? [buildTraceItemOwner(manifest, query, 'call', call.id, `${call.method || ''} ${call.path || call.label}`.trim())]
1202
+ : [];
1203
+ }),
420
1204
  events: request.events.flatMap((event) => {
421
- const query = getEventQuery(event);
422
- return query ? [buildTraceItemOwner(manifest, query, 'event', `${event.index}`, `${event.type}`)] : [];
423
- }),
1205
+ const query = getEventQuery(event);
1206
+ return query ? [buildTraceItemOwner(manifest, query, 'event', `${event.index}`, `${event.type}`)] : [];
1207
+ }),
424
1208
  primary,
425
1209
  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
- }),
1210
+ const ownerQuery = query.ownerFilepath || query.callerPath || query.callerLabel || query.query;
1211
+ return ownerQuery
1212
+ ? [buildTraceItemOwner(manifest, ownerQuery, 'sql', query.id, `${query.operation} ${query.model || query.callerPath || 'query'}`)]
1213
+ : [];
1214
+ }),
431
1215
  };
432
1216
  };
433
1217
 
@@ -449,6 +1233,8 @@ export const buildDiagnoseResponse = ({
449
1233
  const owner = explainOwner(manifest, query);
450
1234
  const attribution = buildTraceAttribution(manifest, request);
451
1235
  const suspects = new Map<string, TSuspectAccumulator>();
1236
+ const orientation = buildOrientationResponse(manifest, query);
1237
+ const chain = buildRequestChain({ manifest, owner, request });
452
1238
 
453
1239
  addSuspect(suspects, owner.matches[0], 5, 'owner query');
454
1240
  addSuspect(suspects, attribution?.primary?.owner, 8, 'primary request');
@@ -468,6 +1254,12 @@ export const buildDiagnoseResponse = ({
468
1254
  doctor,
469
1255
  explainSummary: buildExplainSummaryItems(manifest),
470
1256
  owner,
1257
+ orientation: {
1258
+ guidance: orientation.guidance,
1259
+ connected: orientation.connected,
1260
+ nextSteps: orientation.nextSteps,
1261
+ },
1262
+ chain,
471
1263
  query,
472
1264
  request,
473
1265
  serverLogs,