@saptools/service-flow 0.1.50 → 0.1.52

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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +16 -3
  3. package/TECHNICAL-NOTE.md +10 -1
  4. package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
  5. package/dist/chunk-PTLDSHRC.js.map +1 -0
  6. package/dist/cli.js +318 -510
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +59 -17
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/000-clean.ts +82 -0
  12. package/src/cli.ts +97 -57
  13. package/src/db/connection.ts +1 -1
  14. package/src/db/migrations.ts +5 -3
  15. package/src/db/repositories.ts +120 -22
  16. package/src/db/schema.ts +7 -2
  17. package/src/index.ts +1 -1
  18. package/src/indexer/repository-indexer.ts +57 -29
  19. package/src/indexer/workspace-indexer.ts +84 -6
  20. package/src/linker/cross-repo-linker.ts +22 -2
  21. package/src/linker/service-resolver.ts +15 -4
  22. package/src/output/table-output.ts +56 -3
  23. package/src/parsers/cds-parser.ts +8 -2
  24. package/src/parsers/decorator-parser.ts +382 -48
  25. package/src/parsers/handler-registration-parser.ts +38 -21
  26. package/src/parsers/imported-wrapper-parser.ts +18 -5
  27. package/src/parsers/outbound-call-parser.ts +25 -8
  28. package/src/parsers/package-json-parser.ts +36 -11
  29. package/src/parsers/service-binding-parser-helpers.ts +8 -1
  30. package/src/parsers/service-binding-parser.ts +6 -3
  31. package/src/parsers/symbol-parser.ts +13 -3
  32. package/src/parsers/ts-project.ts +54 -0
  33. package/src/trace/000-dynamic-target-types.ts +84 -0
  34. package/src/trace/001-dynamic-identity.ts +280 -0
  35. package/src/trace/002-trace-diagnostics.ts +54 -0
  36. package/src/trace/003-dynamic-references.ts +82 -0
  37. package/src/trace/dynamic-branches.ts +45 -0
  38. package/src/trace/dynamic-targets.ts +654 -0
  39. package/src/trace/evidence.ts +391 -22
  40. package/src/trace/selectors.ts +483 -0
  41. package/src/trace/trace-engine.ts +101 -94
  42. package/src/types.ts +39 -1
  43. package/dist/chunk-52OUS3MO.js.map +0 -1
@@ -1,14 +1,27 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import { reposByName } from '../db/repositories.js';
2
3
  import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
3
4
  import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
4
5
  import { resolveOperation } from '../linker/service-resolver.js';
5
- import type { ImplementationHint, TraceEdge, TraceResult, TraceStart } from '../types.js';
6
- import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
6
+ import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
7
+ import { baseTraceEvidence, edgeTarget, runtimeNoCandidateDiagnostics, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
8
+ import { dynamicCandidateBranches } from './dynamic-branches.js';
9
+ import {
10
+ loadTraceDiagnostics,
11
+ prependTraceDiagnostic,
12
+ } from './002-trace-diagnostics.js';
7
13
  import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
8
- import { ambiguousStartDiagnostic } from './selectors.js';
14
+ import {
15
+ ambiguousStartDiagnostic,
16
+ selectorNotFoundDiagnostic,
17
+ selectorRepoAmbiguousDiagnostic,
18
+ selectorRepoNotFoundDiagnostic,
19
+ sourceScopeForSelector,
20
+ } from './selectors.js';
9
21
  interface RepoRef {
10
22
  id: number;
11
23
  name: string;
24
+ packageName?: string;
12
25
  }
13
26
  interface StartScope {
14
27
  repo?: RepoRef;
@@ -77,13 +90,14 @@ function normalizeOperation(value: string | undefined): string | undefined {
77
90
  function positiveDepth(value: number): number {
78
91
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
79
92
  }
80
- function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, hintOptions: ImplementationHintOptions): { files?: Set<string>; symbols?: Set<number>; repoId?: number; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
93
+ function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, hintOptions: ImplementationHintOptions, workspaceId?: number): { files?: Set<string>; symbols?: Set<number>; repoId?: number; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
81
94
  const requested = normalizeOperation(start.operationPath ?? start.operation);
82
95
  if (!requested) return undefined;
83
96
  const rows = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.service_path servicePath,r.id repoId,r.name repoName
84
97
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
85
- WHERE (? IS NULL OR r.id=?) AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
86
- ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith('/') ? requested : `/${requested}`) as Array<Record<string, unknown>>;
98
+ WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR r.id=?)
99
+ AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
100
+ ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(workspaceId, workspaceId, repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith('/') ? requested : `/${requested}`) as Array<Record<string, unknown>>;
87
101
  if (rows.length === 0) return undefined;
88
102
  const repoCount = new Set(rows.map((row) => String(row.repoName))).size;
89
103
  const serviceCount = new Set(rows.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
@@ -125,60 +139,33 @@ function sourceFilesForStart(
125
139
  db: Db,
126
140
  repoId: number | undefined,
127
141
  start: TraceStart,
128
- ): { files?: Set<string>; symbols?: Set<number>; repoId?: number } | undefined {
129
- const handler = start.handler;
130
- const operation = normalizeOperation(start.operation ?? start.operationPath);
131
- if (!handler && !operation) return undefined;
132
- const rows = db
133
- .prepare(
134
- `SELECT DISTINCT hc.source_file sourceFile,s.id symbolId
135
- FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name
136
- WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
137
- AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
138
- AND (? IS NULL OR EXISTS (SELECT 1 FROM cds_services s JOIN cds_operations o ON o.service_id=s.id WHERE s.repo_id=hc.repo_id AND s.service_path=? AND (? IS NULL OR o.operation_path=? OR o.operation_name=? OR hm.decorator_value=? OR hm.method_name=?)))`,
139
- )
140
- .all(
141
- repoId,
142
- repoId,
143
- handler,
144
- handler,
145
- handler,
146
- operation,
147
- operation,
148
- operation,
149
- start.servicePath,
150
- start.servicePath,
151
- operation,
152
- operation,
153
- operation,
154
- operation,
155
- operation,
156
- ) as Array<{ sourceFile?: string; symbolId?: number }>;
157
- if (rows.length > 0) return { files: new Set(rows.map((row) => row.sourceFile).filter(Boolean) as string[]), symbols: new Set(rows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
158
- if (start.servicePath && operation) {
159
- const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
160
- FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
161
- JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved' AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
162
- JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
163
- JOIN handler_classes hc ON hc.id=hm.handler_class_id
164
- LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
165
- WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation) as Array<{ sourceFile?: string; symbolId?: number }>;
166
- if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean) as string[]), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
167
- }
168
- return undefined;
169
- }
170
- function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions): StartScope {
171
- const repo = start.repo
172
- ? (db
173
- .prepare(
174
- 'SELECT id,name FROM repositories WHERE name=? OR package_name=?',
175
- )
176
- .get(start.repo, start.repo) as RepoRef | undefined)
177
- : undefined;
178
- if (start.repo && !repo) return { repo, selectorMatched: false };
179
- const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
142
+ workspaceId: number | undefined,
143
+ ): ReturnType<typeof sourceScopeForSelector> {
144
+ return sourceScopeForSelector(db, repoId, start, workspaceId);
145
+ }
146
+ function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions, workspaceId?: number): StartScope {
147
+ const repos: RepoRef[] = start.repo
148
+ ? reposByName(db, start.repo, workspaceId).map((row) => ({
149
+ id: row.id,
150
+ name: row.name,
151
+ packageName: row.package_name ?? undefined,
152
+ }))
153
+ : [];
154
+ if (start.repo && repos.length === 0) return {
155
+ selectorMatched: false,
156
+ startDiagnostics: [selectorRepoNotFoundDiagnostic(start.repo)],
157
+ };
158
+ if (start.repo && repos.length > 1) return {
159
+ selectorMatched: false,
160
+ startDiagnostics: [selectorRepoAmbiguousDiagnostic(start.repo, repos)],
161
+ };
162
+ const repo = repos[0];
163
+ const operationScope = operationStartScope(
164
+ db, repo?.id, start, hintOptions, workspaceId,
165
+ );
180
166
  const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === 'operation' || d.resolutionStage === 'implementation');
181
- const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
167
+ const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start, workspaceId);
168
+ const terminalSelectorScope = Boolean(sourceScope?.diagnostics?.length && !sourceScope.files);
182
169
  const sourceFiles = sourceScope?.files;
183
170
  const hasSelector = Boolean(
184
171
  start.handler ?? start.operation ?? start.operationPath ?? start.servicePath,
@@ -190,9 +177,12 @@ function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOp
190
177
  executionRepoId: sourceScope?.repoId ?? repo?.id,
191
178
  sourceFiles,
192
179
  symbolIds: sourceScope?.symbols,
193
- selectorMatched: !terminalOperationScope && (!hasSelector || sourceFiles !== undefined),
180
+ selectorMatched: !terminalOperationScope && !terminalSelectorScope
181
+ && (!hasSelector || sourceFiles !== undefined),
194
182
  startOperationId: operationScope?.operationId,
195
- startDiagnostics: operationScope?.diagnostics,
183
+ startDiagnostics: operationScope?.diagnostics?.length
184
+ ? operationScope.diagnostics
185
+ : sourceScope?.diagnostics,
196
186
  };
197
187
  }
198
188
  function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
@@ -210,8 +200,19 @@ function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
210
200
  .prepare(
211
201
  `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
212
202
  JOIN handler_methods hm ON hm.handler_class_id=hc.id
213
- LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
214
- WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`,
203
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
204
+ AND sym.source_file=hc.source_file
205
+ AND sym.qualified_name=hc.class_name || '.' || hm.method_name
206
+ AND sym.start_line=hm.source_line
207
+ WHERE hc.repo_id=?
208
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
209
+ CASE WHEN hm.decorator_kind='Event' THEN 'event'
210
+ WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
211
+ ELSE 'unsupported' END)='operation'
212
+ AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
213
+ CASE WHEN hm.decorator_kind IN ('Action','Func','On')
214
+ THEN 1 ELSE 0 END)=1
215
+ AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`,
215
216
  )
216
217
  .all(op.repoId, operation, operation, op.operationName) as Array<{
217
218
  sourceFile?: string;
@@ -229,7 +230,9 @@ function handlerMethodNode(db: Db, methodId: string): Record<string, unknown> |
229
230
  function implementationScope(db: Db, operationId: string): { repoId?: number; files: Set<string>; symbolId?: number; edge?: GraphRow } {
230
231
  const edge = implementationEdge(db, operationId);
231
232
  if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
232
- const row = db.prepare('SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?').get(edge.to_id) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
233
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.qualified_name=hc.class_name || '.' || hm.method_name AND s.start_line=hm.source_line WHERE hm.id=?").get(edge.to_id) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
234
+ if (!row || typeof row.symbolId !== 'number')
235
+ return { repoId: row?.repoId, files: new Set(), edge };
233
236
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
234
237
  }
235
238
  function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
@@ -256,8 +259,8 @@ function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId:
256
259
  return { blocksAutomatic: false, evidence: hinted.evidence.reason === 'no_scoped_hint_matched_edge' ? hinted.evidence : { status: 'tied', tieReason: scores.length > 1 ? 'duplicate_helper_implementation_candidates' : 'no_unique_materially_stronger_candidate', candidateScores: scores } };
257
260
  }
258
261
  function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
259
- const row = db.prepare('SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?').get(methodId) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
260
- if (!row) return undefined;
262
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.qualified_name=hc.class_name || '.' || hm.method_name AND s.start_line=hm.source_line WHERE hm.id=?").get(methodId) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
263
+ if (!row || typeof row.symbolId !== 'number') return undefined;
261
264
  return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
262
265
  }
263
266
 
@@ -307,6 +310,7 @@ function operationNode(db: Db, operationId: string): Record<string, unknown> | u
307
310
  if (!row) return undefined;
308
311
  return { id: `operation:${operationId}`, kind: 'operation', label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
309
312
  }
313
+
310
314
  function workspaceIdForCall(db: Db, callId: string): number | undefined {
311
315
  return (db.prepare('SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?').get(callId) as { workspaceId?: number } | undefined)?.workspaceId;
312
316
  }
@@ -486,33 +490,28 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
486
490
  export function trace(
487
491
  db: Db,
488
492
  start: TraceStart,
489
- options: {
490
- depth: number;
491
- vars?: Record<string, string>;
492
- includeExternal?: boolean;
493
- includeDb?: boolean;
494
- includeAsync?: boolean;
495
- implementationRepo?: string;
496
- implementationHints?: ImplementationHint[];
497
- },
493
+ options: TraceOptions,
498
494
  ): TraceResult {
499
495
  const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
500
- const scope = startScope(db, start, hintOptions);
501
- const diagnostics = db
502
- .prepare(
503
- 'SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)',
504
- )
505
- .all(scope.repo?.id, scope.repo?.id) as Array<Record<string, unknown>>;
506
- const stale = db.prepare('SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?)').all(scope.repo?.id, scope.repo?.id) as Array<{ name?: string; reason?: string }>;
496
+ const scope = startScope(db, start, hintOptions, options.workspaceId);
497
+ const hasSelector = Boolean(start.repo || start.handler || start.operation
498
+ || start.operationPath || start.servicePath);
499
+ const diagnosticRepoId = scope.executionRepoId ?? scope.repo?.id;
500
+ const diagnostics = loadTraceDiagnostics(
501
+ db,
502
+ diagnosticRepoId,
503
+ !hasSelector,
504
+ options.workspaceId,
505
+ );
506
+ const stale = diagnosticRepoId !== undefined || !hasSelector
507
+ ? db.prepare('SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?) AND (? IS NULL OR workspace_id=?) ORDER BY name,id').all(diagnosticRepoId, diagnosticRepoId, options.workspaceId, options.workspaceId) as Array<{ name?: string; reason?: string }>
508
+ : [];
507
509
  for (const row of stale)
508
- diagnostics.unshift({ severity: 'warning', code: 'graph_stale', message: `Graph is stale for ${row.name ?? 'repository'}: ${row.reason ?? 'facts_changed'}. Run service-flow link.` });
509
- for (const diagnostic of scope.startDiagnostics ?? []) diagnostics.unshift(diagnostic);
510
+ prependTraceDiagnostic(diagnostics, { severity: 'warning', code: 'graph_stale', message: `Graph is stale for ${row.name ?? 'repository'}: ${row.reason ?? 'facts_changed'}. Run service-flow link.` });
511
+ for (const diagnostic of scope.startDiagnostics ?? [])
512
+ prependTraceDiagnostic(diagnostics, diagnostic);
510
513
  if (!scope.selectorMatched && !(scope.startDiagnostics?.length))
511
- diagnostics.unshift({
512
- severity: 'warning',
513
- code: 'trace_start_not_found',
514
- message: start.servicePath && !start.operation && !start.operationPath && !start.handler ? 'Service-only trace requires --operation or --path and will not broaden to the whole workspace' : 'No handler source matched the requested trace start selector',
515
- });
514
+ prependTraceDiagnostic(diagnostics, selectorNotFoundDiagnostic(start));
516
515
  const maxDepth = positiveDepth(options.depth);
517
516
  const edges: TraceEdge[] = [];
518
517
  const nodes = new Map<string, Record<string, unknown>>();
@@ -545,9 +544,9 @@ export function trace(
545
544
  seenScopes.add(key);
546
545
  const calls = db
547
546
  .prepare(
548
- `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`,
547
+ `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR r.workspace_id=?) ORDER BY c.source_file,c.source_line`,
549
548
  )
550
- .all(current.repoId, current.repoId) as CallRow[];
549
+ .all(current.repoId, current.repoId, options.workspaceId, options.workspaceId) as CallRow[];
551
550
  const filtered = calls.filter(
552
551
  (c) =>
553
552
  (!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) &&
@@ -595,7 +594,11 @@ export function trace(
595
594
  String(row.evidence_json || '{}'),
596
595
  ) as Record<string, unknown>;
597
596
  const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
598
- const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, options.vars, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
597
+ const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
598
+ vars: options.vars,
599
+ dynamicMode: options.dynamicMode ?? 'strict',
600
+ maxDynamicCandidates: options.maxDynamicCandidates,
601
+ }, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
599
602
  const evidence = effective.evidence;
600
603
  const effectiveRow = effective.row;
601
604
  const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
@@ -615,6 +618,8 @@ export function trace(
615
618
  confidence: Number(effectiveRow.confidence ?? call.confidence),
616
619
  unresolvedReason: effective.unresolvedReason,
617
620
  });
621
+ if ((options.dynamicMode ?? 'strict') === 'candidates')
622
+ edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
618
623
  if (effectiveRow.to_kind === 'operation') {
619
624
  const implementation = implementationScope(db, effectiveRow.to_id);
620
625
  const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
@@ -623,7 +628,7 @@ export function trace(
623
628
  if (implementation.edge) {
624
629
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
625
630
  const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
626
- if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
631
+ if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
627
632
  const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
628
633
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
629
634
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
@@ -674,6 +679,8 @@ export function trace(
674
679
  }
675
680
  }
676
681
  const runtimeDiagnostic = runtimeVariableDiagnostic(edges);
677
- if (runtimeDiagnostic) diagnostics.unshift(runtimeDiagnostic);
682
+ if (runtimeDiagnostic) prependTraceDiagnostic(diagnostics, runtimeDiagnostic);
683
+ for (const diagnostic of runtimeNoCandidateDiagnostics(edges))
684
+ prependTraceDiagnostic(diagnostics, diagnostic);
678
685
  return { start, nodes: [...nodes.values()], edges, diagnostics };
679
686
  }
package/src/types.ts CHANGED
@@ -95,14 +95,34 @@ export interface HandlerClassFact {
95
95
  sourceFile: string;
96
96
  sourceLine: number;
97
97
  methods: HandlerMethodFact[];
98
- }
98
+ hasHandlerDecorator?: boolean;
99
+ classDecoratorNames?: string[];
100
+ observedDecoratorNames?: string[];
101
+ unsupportedDecoratorNames?: string[];
102
+ }
103
+ export type HandlerMethodKind =
104
+ | 'operation'
105
+ | 'entity_lifecycle'
106
+ | 'event'
107
+ | 'unsupported_lifecycle'
108
+ | 'unsupported_decorator';
109
+ export type HandlerLifecyclePhase = 'on' | 'before' | 'after';
110
+ export type HandlerLifecycleEvent = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE';
99
111
  export interface HandlerMethodFact {
100
112
  methodName: string;
101
113
  decoratorKind: string;
102
114
  decoratorValue?: string;
103
115
  decoratorRawExpression: string;
116
+ handlerKind?: HandlerMethodKind;
117
+ executable?: boolean;
118
+ lifecyclePhase?: HandlerLifecyclePhase;
119
+ lifecycleEvent?: HandlerLifecycleEvent;
104
120
  decoratorResolution: {
105
121
  rawExpression: string;
122
+ decoratorExpression?: string;
123
+ argumentExpression?: string;
124
+ resolvedDecoratorKind?: string;
125
+ decoratorImportSource?: string;
106
126
  resolvedValue?: string;
107
127
  resolutionKind:
108
128
  | 'literal'
@@ -110,8 +130,13 @@ export interface HandlerMethodFact {
110
130
  | 'enum_member'
111
131
  | 'const_object_property'
112
132
  | 'generated_constant_name'
133
+ | 'lifecycle_implicit'
113
134
  | 'unresolved';
114
135
  unresolvedReason?: string;
136
+ handlerKind?: HandlerMethodKind;
137
+ executable?: boolean;
138
+ lifecyclePhase?: HandlerLifecyclePhase;
139
+ lifecycleEvent?: HandlerLifecycleEvent;
115
140
  };
116
141
  sourceFile: string;
117
142
  sourceLine: number;
@@ -199,6 +224,19 @@ export interface ImplementationHint {
199
224
  candidateFamily?: string;
200
225
  implementationRepo: string;
201
226
  }
227
+ export type DynamicMode = 'strict' | 'candidates' | 'infer';
228
+ export interface TraceOptions {
229
+ depth: number;
230
+ workspaceId?: number;
231
+ vars?: Record<string, string>;
232
+ includeExternal?: boolean;
233
+ includeDb?: boolean;
234
+ includeAsync?: boolean;
235
+ implementationRepo?: string;
236
+ implementationHints?: ImplementationHint[];
237
+ dynamicMode?: DynamicMode;
238
+ maxDynamicCandidates?: number;
239
+ }
202
240
  export interface TraceEdge {
203
241
  step: number;
204
242
  type: string;