@saptools/service-flow 0.1.52 → 0.1.53

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 (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +9 -12
  3. package/TECHNICAL-NOTE.md +11 -3
  4. package/dist/{chunk-PTLDSHRC.js → chunk-LFH7C46B.js} +2214 -1044
  5. package/dist/chunk-LFH7C46B.js.map +1 -0
  6. package/dist/cli.js +157 -11
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/package.json +1 -1
  10. package/src/cli/001-doctor-projection.ts +140 -0
  11. package/src/cli/doctor.ts +20 -3
  12. package/src/db/repositories.ts +10 -2
  13. package/src/linker/000-implementation-candidates.ts +641 -0
  14. package/src/linker/001-implementation-evidence-projection.ts +119 -0
  15. package/src/linker/002-call-evidence.ts +226 -0
  16. package/src/linker/cross-repo-linker.ts +27 -453
  17. package/src/linker/dynamic-edge-resolver.ts +35 -0
  18. package/src/linker/external-http-target.ts +24 -3
  19. package/src/linker/helper-package-linker.ts +18 -2
  20. package/src/linker/service-resolver.ts +45 -2
  21. package/src/output/doctor-output.ts +13 -4
  22. package/src/output/table-output.ts +4 -2
  23. package/src/trace/000-dynamic-target-types.ts +12 -0
  24. package/src/trace/001-dynamic-identity.ts +45 -17
  25. package/src/trace/003-dynamic-references.ts +236 -35
  26. package/src/trace/004-dynamic-candidate-sources.ts +155 -0
  27. package/src/trace/005-implementation-selection.ts +187 -0
  28. package/src/trace/006-contextual-projection.ts +30 -0
  29. package/src/trace/007-implementation-start-diagnostic.ts +61 -0
  30. package/src/trace/dynamic-targets.ts +205 -159
  31. package/src/trace/evidence.ts +35 -9
  32. package/src/trace/implementation-hints.ts +148 -8
  33. package/src/trace/selectors.ts +74 -9
  34. package/src/trace/trace-engine.ts +39 -52
  35. package/src/utils/000-bounded-projection.ts +161 -0
  36. package/dist/chunk-PTLDSHRC.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { ImplementationHint } from '../types.js';
2
+ import { projectBounded } from '../utils/000-bounded-projection.js';
2
3
 
3
4
  interface Candidate {
4
5
  accepted?: boolean;
@@ -25,6 +26,13 @@ export interface ImplementationSelection {
25
26
  evidence: Record<string, unknown>;
26
27
  }
27
28
 
29
+ export interface ImplementationHintSuggestionProjection {
30
+ suggestions: Array<Record<string, unknown>>;
31
+ suggestionCount: number;
32
+ shownSuggestionCount: number;
33
+ omittedSuggestionCount: number;
34
+ }
35
+
28
36
  export function parseImplementationHint(value: string): ImplementationHint {
29
37
  const hint: Partial<ImplementationHint> = {};
30
38
  for (const part of value.split(',')) {
@@ -40,8 +48,9 @@ export function selectImplementation(
40
48
  rawEvidence: Record<string, unknown>,
41
49
  hints: ImplementationHint[] | undefined,
42
50
  legacyRepo: string | undefined,
51
+ canonicalEvidence?: Record<string, unknown>,
43
52
  ): ImplementationSelection {
44
- const evidence = asEvidence(rawEvidence);
53
+ const evidence = asEvidence(canonicalEvidence ?? rawEvidence);
45
54
  const scoped = hints ?? [];
46
55
  const matchingHints = scoped.filter((hint) => hintMatchesEdge(hint, evidence));
47
56
  if (matchingHints.length === 0) {
@@ -50,14 +59,18 @@ export function selectImplementation(
50
59
  return { blocksAutomatic: false, evidence: { status: 'not_matched', reason, strategy: 'scoped_implementation_hint' } };
51
60
  }
52
61
  if (matchingHints.length > 1) {
62
+ const projection = projectBounded(matchingHints, compareHints);
53
63
  return {
54
64
  blocksAutomatic: true,
55
65
  evidence: {
56
66
  status: 'tied',
57
67
  reason: 'multiple_scoped_hints_matched_edge',
58
68
  strategy: 'scoped_implementation_hint',
59
- matchedHints: matchingHints,
69
+ matchedHints: projection.items,
60
70
  candidateCount: matchingHints.length,
71
+ matchedHintCount: projection.totalCount,
72
+ shownMatchedHintCount: projection.shownCount,
73
+ omittedMatchedHintCount: projection.omittedCount,
61
74
  },
62
75
  };
63
76
  }
@@ -67,26 +80,48 @@ export function selectImplementation(
67
80
 
68
81
  export function implementationHintDiagnostic(
69
82
  selection: ImplementationSelection,
70
- suggestions?: unknown,
83
+ suggestionEvidence?: unknown,
71
84
  ): Record<string, unknown> | undefined {
72
85
  if (!selection.blocksAutomatic || selection.methodId) return undefined;
86
+ const suggestions = projectedSuggestions(suggestionEvidence);
73
87
  return {
74
88
  severity: 'warning',
75
89
  code: 'implementation_hint_mismatch',
76
90
  message: 'Implementation hint did not select exactly one viable candidate',
77
91
  hintStatus: selection.evidence.status,
78
92
  candidateCount: selection.evidence.candidateCount,
79
- implementationHintSuggestions: Array.isArray(suggestions) && suggestions.length > 0 ? suggestions : undefined,
93
+ implementationHintSuggestions: suggestions.suggestions.length > 0
94
+ ? suggestions.suggestions
95
+ : undefined,
96
+ implementationHintSuggestionCount: suggestions.suggestionCount,
97
+ shownImplementationHintSuggestionCount: suggestions.shownSuggestionCount,
98
+ omittedImplementationHintSuggestionCount: suggestions.omittedSuggestionCount,
80
99
  implementationSelection: selection.evidence,
81
100
  };
82
101
  }
83
102
 
84
103
  export function implementationHintSuggestions(rawEvidence: Record<string, unknown>): Array<Record<string, unknown>> {
104
+ return implementationHintSuggestionProjection(rawEvidence).suggestions;
105
+ }
106
+
107
+ export function implementationHintSuggestionProjection(
108
+ rawEvidence: Record<string, unknown>,
109
+ ): ImplementationHintSuggestionProjection {
85
110
  const evidence = asEvidence(rawEvidence);
86
111
  const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
87
- if (accepted.length < 2) return [];
112
+ if (accepted.length < 2) {
113
+ return {
114
+ suggestions: [],
115
+ suggestionCount: 0,
116
+ shownSuggestionCount: 0,
117
+ omittedSuggestionCount: 0,
118
+ };
119
+ }
88
120
  const repos = selectableRepositories(accepted);
89
- return accepted
121
+ const repositoryProjection = projectBounded(
122
+ repos, (left, right) => left.localeCompare(right),
123
+ );
124
+ const suggestions = accepted
90
125
  .flatMap((candidate) => {
91
126
  const repo = candidate.handlerPackage?.name;
92
127
  if (!repo || !repos.includes(repo)) return [];
@@ -96,12 +131,71 @@ export function implementationHintSuggestions(rawEvidence: Record<string, unknow
96
131
  operationPath: hint.operationPath,
97
132
  ambiguityReason: evidence.ambiguityReasons?.[0],
98
133
  candidateFamily: hint.candidateFamily,
99
- selectableImplementationRepositories: repos,
134
+ selectableImplementationRepositories: repositoryProjection.items,
135
+ selectableImplementationRepositoryCount: repositoryProjection.totalCount,
136
+ shownSelectableImplementationRepositoryCount:
137
+ repositoryProjection.shownCount,
138
+ omittedSelectableImplementationRepositoryCount:
139
+ repositoryProjection.omittedCount,
100
140
  implementationRepo: repo,
101
141
  hint,
102
142
  cli: `--implementation-hint ${hintString(hint)}`,
103
143
  }];
104
144
  });
145
+ const projection = projectBounded(suggestions, compareSuggestion);
146
+ return {
147
+ suggestions: projection.items,
148
+ suggestionCount: projection.totalCount,
149
+ shownSuggestionCount: projection.shownCount,
150
+ omittedSuggestionCount: projection.omittedCount,
151
+ };
152
+ }
153
+
154
+ function projectedSuggestions(value: unknown): ImplementationHintSuggestionProjection {
155
+ const evidence = objectRecord(value);
156
+ const values = Array.isArray(value)
157
+ ? recordSuggestions(value)
158
+ : recordSuggestions(evidence.implementationHintSuggestions);
159
+ const projection = projectBounded(values, compareSuggestion);
160
+ const total = Math.max(
161
+ numericValue(evidence.implementationHintSuggestionCount),
162
+ projection.totalCount,
163
+ );
164
+ return {
165
+ suggestions: projection.items,
166
+ suggestionCount: total,
167
+ shownSuggestionCount: projection.shownCount,
168
+ omittedSuggestionCount: Math.max(0, total - projection.shownCount),
169
+ };
170
+ }
171
+
172
+ function compareHints(left: ImplementationHint, right: ImplementationHint): number {
173
+ return hintString(left).localeCompare(hintString(right));
174
+ }
175
+
176
+ function compareSuggestion(
177
+ left: Record<string, unknown>,
178
+ right: Record<string, unknown>,
179
+ ): number {
180
+ return String(left.cli ?? '').localeCompare(String(right.cli ?? ''))
181
+ || String(left.implementationRepo ?? '').localeCompare(
182
+ String(right.implementationRepo ?? ''),
183
+ );
184
+ }
185
+
186
+ function objectRecord(value: unknown): Record<string, unknown> {
187
+ return isRecord(value) ? value : {};
188
+ }
189
+
190
+ function recordSuggestions(value: unknown): Array<Record<string, unknown>> {
191
+ return Array.isArray(value)
192
+ ? value.filter((item): item is Record<string, unknown> =>
193
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
194
+ : [];
195
+ }
196
+
197
+ function numericValue(value: unknown): number {
198
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
105
199
  }
106
200
 
107
201
  function selectableRepositories(candidates: Candidate[]): string[] {
@@ -219,5 +313,51 @@ function legacyHint(implementationRepo: string): ImplementationHint {
219
313
  }
220
314
 
221
315
  function asEvidence(value: Record<string, unknown>): EdgeEvidence {
222
- return value as EdgeEvidence;
316
+ return {
317
+ servicePath: stringValue(value.servicePath),
318
+ operationPath: stringValue(value.operationPath),
319
+ ambiguityReasons: stringArray(value.ambiguityReasons),
320
+ candidateFamilies: candidateFamilies(value.candidateFamilies),
321
+ candidates: candidates(value.candidates),
322
+ modelPackage: packageValue(value.modelPackage),
323
+ };
324
+ }
325
+
326
+ function candidates(value: unknown): Candidate[] {
327
+ return recordSuggestions(value).map((candidate) => ({
328
+ accepted: candidate.accepted === true,
329
+ methodId: numericValue(candidate.methodId) || undefined,
330
+ sourceFile: stringValue(candidate.sourceFile),
331
+ handlerPackage: packageValue(candidate.handlerPackage),
332
+ modelPackage: packageValue(candidate.modelPackage),
333
+ servicePath: stringValue(candidate.servicePath),
334
+ operationPath: stringValue(candidate.operationPath),
335
+ }));
336
+ }
337
+
338
+ function candidateFamilies(value: unknown): Array<{ packageName?: string }> {
339
+ return recordSuggestions(value).map((family) => ({
340
+ packageName: stringValue(family.packageName),
341
+ }));
342
+ }
343
+
344
+ function packageValue(value: unknown): { name?: string; packageName?: string } | undefined {
345
+ const candidate = objectRecord(value);
346
+ const name = stringValue(candidate.name);
347
+ const packageName = stringValue(candidate.packageName);
348
+ return name || packageName ? { name, packageName } : undefined;
349
+ }
350
+
351
+ function stringArray(value: unknown): string[] {
352
+ return Array.isArray(value)
353
+ ? value.filter((item): item is string => typeof item === 'string')
354
+ : [];
355
+ }
356
+
357
+ function stringValue(value: unknown): string | undefined {
358
+ return typeof value === 'string' ? value : undefined;
359
+ }
360
+
361
+ function isRecord(value: unknown): value is Record<string, unknown> {
362
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
223
363
  }
@@ -1,5 +1,9 @@
1
1
  import type { Db } from '../db/connection.js';
2
2
  import type { TraceStart } from '../types.js';
3
+ import {
4
+ projectBounded,
5
+ type BoundedProjection,
6
+ } from '../utils/000-bounded-projection.js';
3
7
 
4
8
  export interface SelectorSourceScope {
5
9
  files?: Set<string>;
@@ -65,14 +69,27 @@ export function selectorRepoAmbiguousDiagnostic(
65
69
  return [`--repo ${candidate.packageName}`];
66
70
  return [];
67
71
  });
72
+ const candidateProjection = projectBounded(candidates, (left, right) =>
73
+ left.name.localeCompare(right.name)
74
+ || String(left.packageName ?? '').localeCompare(String(right.packageName ?? ''))
75
+ || left.id - right.id);
76
+ const suggestionProjection = projectBounded(
77
+ [...new Set(suggestions)], (left, right) => left.localeCompare(right),
78
+ );
68
79
  return {
69
80
  severity: 'warning',
70
81
  code: 'selector_repo_ambiguous',
71
82
  message: `Repository selector matched multiple indexed repositories: ${requested}`,
72
83
  selectorKind: 'repo',
73
84
  requestedRepository: requested,
74
- candidates,
75
- selectorSuggestions: [...new Set(suggestions)].sort(),
85
+ candidates: candidateProjection.items,
86
+ candidateCount: candidateProjection.totalCount,
87
+ shownCandidateCount: candidateProjection.shownCount,
88
+ omittedCandidateCount: candidateProjection.omittedCount,
89
+ selectorSuggestions: suggestionProjection.items,
90
+ selectorSuggestionCount: suggestionProjection.totalCount,
91
+ shownSelectorSuggestionCount: suggestionProjection.shownCount,
92
+ omittedSelectorSuggestionCount: suggestionProjection.omittedCount,
76
93
  remediation: suggestions.length > 0
77
94
  ? 'Use one of the unique --repo selectors shown.'
78
95
  : 'Repository names and package names must be unique before this selector can be traced safely.',
@@ -250,6 +267,8 @@ function operationHandlerScope(
250
267
  ? [`--repo ${candidate.repoName} --handler ${candidate.className}`]
251
268
  : [];
252
269
  });
270
+ const projection = boundedSelectorCandidates(candidates);
271
+ const suggestionProjection = boundedSelectorSuggestions(suggestions);
253
272
  return { diagnostics: [{
254
273
  severity: 'warning',
255
274
  code: 'trace_start_ambiguous',
@@ -258,8 +277,14 @@ function operationHandlerScope(
258
277
  normalizedSelectorValue: requested,
259
278
  resolutionStage: 'handler',
260
279
  resolutionStatus: 'ambiguous_handler_operation',
261
- candidates,
262
- selectorSuggestions: [...new Set(suggestions)].sort(),
280
+ candidates: projection.items,
281
+ candidateCount: projection.totalCount,
282
+ shownCandidateCount: projection.shownCount,
283
+ omittedCandidateCount: projection.omittedCount,
284
+ selectorSuggestions: suggestionProjection.items,
285
+ selectorSuggestionCount: suggestionProjection.totalCount,
286
+ shownSelectorSuggestionCount: suggestionProjection.shownCount,
287
+ omittedSelectorSuggestionCount: suggestionProjection.omittedCount,
263
288
  remediation: 'Select one handler class explicitly; no operation was chosen automatically.',
264
289
  }] };
265
290
  }
@@ -358,6 +383,8 @@ function handlerSelectorAmbiguity(
358
383
  return [`${repoName ? `--repo ${repoName} ` : ''}--handler ${candidate.className}`];
359
384
  return [];
360
385
  });
386
+ const projection = boundedSelectorCandidates(candidates);
387
+ const suggestionProjection = boundedSelectorSuggestions(suggestions);
361
388
  return {
362
389
  severity: 'warning',
363
390
  code: 'trace_start_ambiguous',
@@ -366,8 +393,14 @@ function handlerSelectorAmbiguity(
366
393
  requestedHandler: requested,
367
394
  resolutionStage: 'handler',
368
395
  resolutionStatus: 'ambiguous_handler',
369
- candidates,
370
- selectorSuggestions: [...new Set(suggestions)].sort(),
396
+ candidates: projection.items,
397
+ candidateCount: projection.totalCount,
398
+ shownCandidateCount: projection.shownCount,
399
+ omittedCandidateCount: projection.omittedCount,
400
+ selectorSuggestions: suggestionProjection.items,
401
+ selectorSuggestionCount: suggestionProjection.totalCount,
402
+ shownSelectorSuggestionCount: suggestionProjection.shownCount,
403
+ omittedSelectorSuggestionCount: suggestionProjection.omittedCount,
371
404
  remediation: suggestions.length > 0
372
405
  ? 'Use one of the scoped handler selectors shown.'
373
406
  : 'No current CLI selector uniquely identifies these duplicate handler classes.',
@@ -510,6 +543,9 @@ export function ambiguousStartDiagnostic(
510
543
  .flatMap((row) => typeof row.servicePath === 'string'
511
544
  ? [`--service ${row.servicePath}`]
512
545
  : []))].sort();
546
+ const projection = boundedSelectorCandidates(candidates);
547
+ const serviceProjection = boundedSelectorSuggestions(serviceSuggestions);
548
+ const selectorProjection = boundedSelectorSuggestions(fullSelectorSuggestions(candidates));
513
549
  return {
514
550
  severity: 'warning',
515
551
  code: 'trace_start_ambiguous',
@@ -517,11 +553,40 @@ export function ambiguousStartDiagnostic(
517
553
  normalizedSelectorValue: requested,
518
554
  resolutionStage: 'operation',
519
555
  resolutionStatus: 'ambiguous_operation',
520
- candidates,
521
- serviceSuggestions,
522
- selectorSuggestions: fullSelectorSuggestions(candidates),
556
+ candidates: projection.items,
557
+ candidateCount: projection.totalCount,
558
+ shownCandidateCount: projection.shownCount,
559
+ omittedCandidateCount: projection.omittedCount,
560
+ serviceSuggestions: serviceProjection.items,
561
+ serviceSuggestionCount: serviceProjection.totalCount,
562
+ shownServiceSuggestionCount: serviceProjection.shownCount,
563
+ omittedServiceSuggestionCount: serviceProjection.omittedCount,
564
+ selectorSuggestions: selectorProjection.items,
565
+ selectorSuggestionCount: selectorProjection.totalCount,
566
+ shownSelectorSuggestionCount: selectorProjection.shownCount,
567
+ omittedSelectorSuggestionCount: selectorProjection.omittedCount,
523
568
  };
524
569
  }
570
+
571
+ function boundedSelectorCandidates(
572
+ candidates: Array<Record<string, unknown>>,
573
+ ): BoundedProjection<Record<string, unknown>> {
574
+ return projectBounded(candidates, (left, right) =>
575
+ String(left.repoName ?? '').localeCompare(String(right.repoName ?? ''))
576
+ || String(left.servicePath ?? '').localeCompare(String(right.servicePath ?? ''))
577
+ || String(left.className ?? '').localeCompare(String(right.className ?? ''))
578
+ || String(left.sourceFile ?? '').localeCompare(String(right.sourceFile ?? ''))
579
+ || Number(left.sourceLine ?? 0) - Number(right.sourceLine ?? 0)
580
+ || Number(left.handlerClassId ?? left.operationId ?? 0)
581
+ - Number(right.handlerClassId ?? right.operationId ?? 0));
582
+ }
583
+
584
+ function boundedSelectorSuggestions(
585
+ suggestions: string[],
586
+ ): BoundedProjection<string> {
587
+ return projectBounded([...new Set(suggestions)], (left, right) =>
588
+ left.localeCompare(right));
589
+ }
525
590
  function fullSelectorSuggestions(
526
591
  candidates: Array<Record<string, unknown>>,
527
592
  ): string[] {
@@ -10,7 +10,14 @@ import {
10
10
  loadTraceDiagnostics,
11
11
  prependTraceDiagnostic,
12
12
  } from './002-trace-diagnostics.js';
13
- import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
13
+ import { implementationHintDiagnostic } from './implementation-hints.js';
14
+ import {
15
+ contextualImplementationSelection,
16
+ hintedImplementationSelection,
17
+ } from './005-implementation-selection.js';
18
+ import { boundedContextCandidates } from './006-contextual-projection.js';
19
+ import { implementationStartDiagnostic } from './007-implementation-start-diagnostic.js';
20
+ import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
14
21
  import {
15
22
  ambiguousStartDiagnostic,
16
23
  selectorNotFoundDiagnostic,
@@ -110,31 +117,21 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
110
117
  const operationId = String(rows[0]?.operationId);
111
118
  const impl = implementationScope(db, operationId);
112
119
  if (impl.edge?.status === 'resolved' && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? new Set([impl.symbolId]) : undefined, repoId: impl.repoId, operationId, diagnostics: [] };
113
- const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
120
+ const hinted = hintedImplementationSelection(
121
+ db, impl.edge, operationId, hintOptions,
122
+ );
114
123
  if (hinted.methodId) {
115
124
  const hintedScope = handlerScope(db, hinted.methodId);
116
125
  if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, repoId: hintedScope.repoId, operationId, diagnostics: [] };
117
126
  }
118
127
  if (impl.edge) {
119
128
  const evidence = parseEvidence(impl.edge.evidence_json);
120
- const hintDiagnostic = implementationHintDiagnostic(hinted, evidence.implementationHintSuggestions);
121
- const diagnostics = [{ severity: 'warning', code: impl.edge.status === 'ambiguous' ? 'trace_start_ambiguous' : 'trace_start_implementation_unresolved', message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? 'unresolved')}`, resolutionStage: 'implementation', resolutionStatus: impl.edge.status === 'ambiguous' ? 'ambiguous_implementation' : 'rejected_implementation', implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, implementationRejectionReasons: implementationRejectionReasons(evidence), implementationHintSuggestions: evidence.implementationHintSuggestions, candidates: evidence.candidates }];
129
+ const hintDiagnostic = implementationHintDiagnostic(hinted, evidence);
130
+ const diagnostics = [implementationStartDiagnostic(impl.edge, evidence)];
122
131
  return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
123
132
  }
124
133
  return { operationId, diagnostics: [{ severity: 'warning', code: 'trace_start_implementation_unresolved', message: 'Indexed operation matched but no implementation candidate exists', resolutionStage: 'implementation', resolutionStatus: 'operation_without_implementation' }] };
125
134
  }
126
- function implementationRejectionReasons(evidence: Record<string, unknown>): string[] {
127
- const candidates = Array.isArray(evidence.candidates)
128
- ? evidence.candidates.filter((item): item is Record<string, unknown> =>
129
- Boolean(item && typeof item === 'object' && !Array.isArray(item)))
130
- : [];
131
- const reasons = candidates.flatMap((candidate) =>
132
- Array.isArray(candidate.rejectedReasons)
133
- ? candidate.rejectedReasons.filter((reason): reason is string =>
134
- typeof reason === 'string')
135
- : []);
136
- return [...new Set(reasons)].sort();
137
- }
138
135
  function sourceFilesForStart(
139
136
  db: Db,
140
137
  repoId: number | undefined,
@@ -235,29 +232,6 @@ function implementationScope(db: Db, operationId: string): { repoId?: number; fi
235
232
  return { repoId: row?.repoId, files: new Set(), edge };
236
233
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
237
234
  }
238
- function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
239
- if (!edge || edge.status !== 'ambiguous') return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
240
- return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
241
- }
242
- function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
243
- const hinted = implementationMethodIdFromHint(edge, hintOptions);
244
- if (hinted.methodId) return hinted;
245
- if (hinted.blocksAutomatic) return hinted;
246
- if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
247
- const evidence = JSON.parse(String(edge.evidence_json || '{}')) as { candidates?: Array<{ accepted?: boolean; methodId?: number; handlerPackage?: { id?: number; name?: string }; applicationPackage?: { id?: number; name?: string }; reasons?: string[]; score?: number }> };
248
- const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
249
- const reasons: string[] = [];
250
- let score = Number(item.score ?? 0);
251
- if (Number(item.handlerPackage?.id) === callerRepoId) { score += 10; reasons.push('handler_package_matches_caller_repository'); }
252
- if (Number(item.applicationPackage?.id) === callerRepoId) { score += 10; reasons.push('registration_package_matches_caller_repository'); }
253
- if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
254
- return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
255
- }).sort((a, b) => b.score - a.score);
256
- if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
257
- const [first, second] = scores;
258
- if (first && first.methodId !== undefined && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), blocksAutomatic: false, evidence: { status: 'selected', selectedMethodId: first.methodId, candidateScores: scores } };
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 } };
260
- }
261
235
  function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
262
236
  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
237
  if (!row || typeof row.symbolId !== 'number') return undefined;
@@ -317,11 +291,15 @@ function workspaceIdForCall(db: Db, callId: string): number | undefined {
317
291
  function parseEvidence(value: unknown): Record<string, unknown> {
318
292
  try {
319
293
  const parsed = JSON.parse(String(value || '{}')) as unknown;
320
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
294
+ return isEvidenceRecord(parsed) ? boundCandidateLikeEvidence(parsed) : {};
321
295
  } catch {
322
296
  return {};
323
297
  }
324
298
  }
299
+
300
+ function isEvidenceRecord(value: unknown): value is Record<string, unknown> {
301
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
302
+ }
325
303
  function receiverFromEvidence(value: unknown): string | undefined {
326
304
  const evidence = parseEvidence(value);
327
305
  return typeof evidence.receiver === 'string' ? evidence.receiver : undefined;
@@ -461,16 +439,20 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
461
439
  function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBinding | undefined, workspaceId: number | undefined, persistedRows: GraphRow[] = []): { row?: GraphRow; evidence?: Record<string, unknown>; unresolvedReason?: string } {
462
440
  if (!binding || String(call.call_type) !== 'remote_action' || call.operation_path_expr === undefined || call.operation_path_expr === null) return {};
463
441
  if (binding.resolutionStatus === 'ambiguous') {
442
+ const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
464
443
  return {
465
444
  evidence: {
466
445
  contextualServiceBindingAttempted: true,
467
446
  contextualBinding: {
468
447
  source: binding.source,
469
448
  status: 'tied',
470
- candidates: binding.bindingCandidates,
449
+ candidates: candidates.candidates,
450
+ candidateCount: candidates.candidateCount,
451
+ shownCandidateCount: candidates.shownCandidateCount,
452
+ omittedCandidateCount: candidates.omittedCandidateCount,
471
453
  },
472
454
  contextualResolutionStatus: 'ambiguous',
473
- contextualCandidateCount: binding.bindingCandidates?.length ?? 0,
455
+ contextualCandidateCount: candidates.candidateCount,
474
456
  },
475
457
  unresolvedReason: 'Ambiguous contextual service binding candidates',
476
458
  };
@@ -480,7 +462,8 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
480
462
  const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
481
463
  const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
482
464
  const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
483
- const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
465
+ const candidates = boundedContextCandidates(resolution.candidates);
466
+ const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: candidates.candidateCount, shownContextualCandidateCount: candidates.shownCandidateCount, omittedContextualCandidateCount: candidates.omittedCandidateCount, candidates: candidates.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
484
467
  if (!resolution.target) return { evidence, unresolvedReason: resolution.status === 'ambiguous' ? 'Ambiguous contextual operation candidates' : resolution.status === 'dynamic' ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(', ')}` : 'No contextual operation candidate matched' };
485
468
  const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
486
469
  const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
@@ -524,7 +507,9 @@ export function trace(
524
507
  const op = operationNode(db, scope.startOperationId);
525
508
  const impl = implementationScope(db, scope.startOperationId);
526
509
  if (op) nodes.set(String(op.id), op);
527
- const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
510
+ const startSelection = hintedImplementationSelection(
511
+ db, impl.edge, scope.startOperationId, hintOptions,
512
+ );
528
513
  if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
529
514
  const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
530
515
  const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: 'indexed_operation_graph', matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : undefined };
@@ -564,7 +549,7 @@ export function trace(
564
549
  const nextKey = `${nextRepoId}:${[...nextSymbols].join(',')}:${[...nextFiles].join(',')}`;
565
550
  const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
566
551
  if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
567
- const evidence = { ...(JSON.parse(String(symbolCall.evidence_json || '{}')) as Record<string, unknown>), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
552
+ const evidence = { ...parseEvidence(symbolCall.evidence_json), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
568
553
  edges.push({ step: current.depth, type: 'local_symbol_call', from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === 'resolved' ? undefined : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : undefined });
569
554
  if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: 'cycle', from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: 'Cycle detected; downstream symbol already visited' });
570
555
  else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
@@ -590,9 +575,7 @@ export function trace(
590
575
  for (const row of graphRows) {
591
576
  if (seenEdges.has(Number(row.id))) continue;
592
577
  seenEdges.add(Number(row.id));
593
- const persistedEvidence = JSON.parse(
594
- String(row.evidence_json || '{}'),
595
- ) as Record<string, unknown>;
578
+ const persistedEvidence = parseEvidence(row.evidence_json);
596
579
  const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
597
580
  const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
598
581
  vars: options.vars,
@@ -618,16 +601,20 @@ export function trace(
618
601
  confidence: Number(effectiveRow.confidence ?? call.confidence),
619
602
  unresolvedReason: effective.unresolvedReason,
620
603
  });
621
- if ((options.dynamicMode ?? 'strict') === 'candidates')
604
+ if ((options.dynamicMode ?? 'strict') === 'candidates'
605
+ && effectiveRow.status !== 'resolved')
622
606
  edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
623
607
  if (effectiveRow.to_kind === 'operation') {
624
608
  const implementation = implementationScope(db, effectiveRow.to_id);
625
- const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
609
+ const contextSelection = contextualImplementationSelection(
610
+ db, implementation.edge, effectiveRow.to_id, current.repoId, evidence,
611
+ hintOptions,
612
+ );
626
613
  const contextMethodId = contextSelection.methodId;
627
614
  const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
628
615
  if (implementation.edge) {
629
- const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
630
- const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
616
+ const implEvidence = parseEvidence(implementation.edge.evidence_json);
617
+ const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence);
631
618
  if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
632
619
  const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
633
620
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;