@saptools/service-flow 0.1.68 → 0.1.69

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 (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +9 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +317 -105
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli.ts +151 -87
  13. package/src/db/000-call-fact-repository.ts +45 -19
  14. package/src/db/003-current-fact-semantics.ts +1 -0
  15. package/src/db/004-package-target-invalidation.ts +10 -0
  16. package/src/db/006-relative-symbol-resolution.ts +28 -7
  17. package/src/db/008-relative-fact-semantics.ts +6 -3
  18. package/src/db/009-binding-fact-semantics.ts +5 -0
  19. package/src/db/012-binding-reference-proof.ts +4 -0
  20. package/src/db/013-index-publication-failure.ts +91 -0
  21. package/src/db/014-binding-helper-provenance.ts +17 -0
  22. package/src/db/repositories.ts +22 -5
  23. package/src/indexer/cds-extension-resolver.ts +27 -3
  24. package/src/indexer/repository-indexer.ts +66 -5
  25. package/src/indexer/workspace-indexer.ts +141 -29
  26. package/src/linker/004-event-subscription-handler-linker.ts +32 -11
  27. package/src/linker/006-event-template-link.ts +72 -0
  28. package/src/linker/007-call-edge-insertion.ts +568 -0
  29. package/src/linker/cross-repo-linker.ts +2 -165
  30. package/src/parsers/000-direct-query-execution.ts +11 -0
  31. package/src/parsers/006-binding-identity.ts +6 -1
  32. package/src/parsers/014-service-binding-helper-flow.ts +70 -4
  33. package/src/parsers/021-binding-visibility.ts +17 -1
  34. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  35. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  36. package/src/parsers/outbound-call-parser.ts +146 -509
  37. package/src/parsers/symbol-parser.ts +37 -5
  38. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  39. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  40. package/src/trace/013-trace-root-scopes.ts +2 -3
  41. package/src/trace/014-compact-contract.ts +6 -0
  42. package/src/trace/015-trace-edge-recorder.ts +61 -4
  43. package/src/trace/016-compact-projector.ts +2 -3
  44. package/src/trace/019-trace-edge-semantics.ts +6 -10
  45. package/src/trace/020-compact-field-projection.ts +74 -1
  46. package/src/trace/021-compact-decision-normalization.ts +75 -9
  47. package/src/trace/024-compact-observation-decision.ts +7 -2
  48. package/src/trace/026-trace-start-scope.ts +1 -0
  49. package/src/trace/027-trace-scope-execution.ts +33 -33
  50. package/src/trace/030-event-runtime-resolution.ts +151 -0
  51. package/src/trace/031-local-call-expansion.ts +37 -0
  52. package/src/trace/implementation-hints.ts +9 -6
  53. package/src/trace/selectors.ts +1 -0
  54. package/src/types.ts +2 -1
  55. package/src/version.ts +1 -1
  56. package/dist/chunk-AEM4JY22.js.map +0 -1
@@ -1,11 +1,14 @@
1
1
  import { compareBinary } from './010-traversal-scope.js';
2
2
  import type {
3
3
  CompactDecisionV1,
4
+ CompactReferenceGroupV1,
4
5
  CompactStatus,
5
6
  } from './014-compact-contract.js';
6
7
 
7
8
  export const COMPACT_MISSING_NAME_LIMIT = 8;
8
9
  export const COMPACT_MISSING_NAME_MAX_LENGTH = 160;
10
+ export const COMPACT_REFERENCE_VALUE_LIMIT = 5;
11
+ const COMPACT_REFERENCE_VALUE_MAX_LENGTH = 240;
9
12
 
10
13
  export interface CompactMissingNameProjection {
11
14
  readonly names: string[];
@@ -23,11 +26,17 @@ export function projectCompactMissingNames(
23
26
  values: readonly string[] | undefined,
24
27
  authoritativeCount: unknown,
25
28
  ): CompactMissingNameProjection {
26
- const raw = [...new Set((values ?? [])
27
- .map((value) => value.trim())
28
- .filter(Boolean))]
29
- .sort(compareBinary);
30
- const safe = raw.filter(isSafeCompactMissingName);
29
+ const candidates = new Map<string, boolean>();
30
+ for (const value of values ?? []) {
31
+ const normalized = value.trim();
32
+ if (!normalized) continue;
33
+ candidates.set(
34
+ normalized,
35
+ (candidates.get(normalized) ?? true) && isSafeCompactMissingName(value),
36
+ );
37
+ }
38
+ const raw = [...candidates.keys()].sort(compareBinary);
39
+ const safe = raw.filter((name) => candidates.get(name) === true);
31
40
  const names = safe.slice(0, COMPACT_MISSING_NAME_LIMIT);
32
41
  const total = Math.max(compactCount(authoritativeCount), raw.length);
33
42
  return {
@@ -37,25 +46,58 @@ export function projectCompactMissingNames(
37
46
  }
38
47
 
39
48
  export function isSafeCompactMissingName(value: string): boolean {
49
+ if (hasControlCharacter(value)) return false;
40
50
  const normalized = value.trim();
41
51
  return normalized.length > 0
42
52
  && normalized.length <= COMPACT_MISSING_NAME_MAX_LENGTH
43
- && !hasControlCharacter(normalized)
44
53
  && safeMissingName.test(normalized);
45
54
  }
46
55
 
56
+ export function projectCompactReferenceGroup(
57
+ values: readonly string[] | undefined,
58
+ authoritativeCount: unknown,
59
+ isSafe: (value: string) => boolean,
60
+ ): CompactReferenceGroupV1 | undefined {
61
+ const candidates = normalizedReferenceValues(values, isSafe);
62
+ const unique = [...candidates.keys()].sort(compareBinary);
63
+ const safe = unique.filter((value) => candidates.get(value) === true);
64
+ const shown = safe.slice(0, COMPACT_REFERENCE_VALUE_LIMIT);
65
+ const total = Math.max(compactCount(authoritativeCount), unique.length);
66
+ if (total === 0) return undefined;
67
+ return {
68
+ values: shown,
69
+ total,
70
+ shown: shown.length,
71
+ omitted: Math.max(0, total - shown.length),
72
+ };
73
+ }
74
+
75
+ export function isSafeCompactReferenceName(value: string): boolean {
76
+ if (!safeReferenceText(value, 160)) return false;
77
+ return /^[@A-Za-z0-9_$][@A-Za-z0-9_$./:-]*$/.test(value.trim());
78
+ }
79
+
80
+ export function isSafeCompactSelectorSuggestion(value: string): boolean {
81
+ if (!safeReferenceText(value, COMPACT_REFERENCE_VALUE_MAX_LENGTH))
82
+ return false;
83
+ return /^(?:--(?:repo|service|operation|path|handler) [@A-Za-z0-9_$./:-]+)(?: --(?:repo|service|operation|path|handler) [@A-Za-z0-9_$./:-]+)*$/
84
+ .test(value.trim());
85
+ }
86
+
47
87
  export function normalizeCompactDecisionEquivalence(
48
88
  decision: CompactDecisionV1,
49
89
  status: CompactStatus,
50
90
  canonicalTarget: string | undefined,
51
91
  targetIdentityEqual = false,
52
92
  persistedIdentityEqual = false,
93
+ rawPersistedStatus?: string,
53
94
  ): void {
95
+ if (decision.effectiveResolutionStatus === status)
96
+ delete decision.effectiveResolutionStatus;
54
97
  removeEquivalentPersistedDecision(
55
98
  decision, status, canonicalTarget, persistedIdentityEqual,
99
+ rawPersistedStatus,
56
100
  );
57
- if (decision.effectiveResolutionStatus === status)
58
- delete decision.effectiveResolutionStatus;
59
101
  if (targetIdentityEqual && canonicalTarget !== undefined
60
102
  && decision.effectiveTarget === canonicalTarget)
61
103
  delete decision.effectiveTarget;
@@ -77,9 +119,10 @@ function removeEquivalentPersistedDecision(
77
119
  status: CompactStatus,
78
120
  canonicalTarget: string | undefined,
79
121
  identityEqual: boolean,
122
+ rawPersistedStatus: string | undefined,
80
123
  ): void {
81
124
  if (!identityEqual
82
- || decision.persistedResolutionStatus !== status
125
+ || rawPersistedStatus !== status
83
126
  || (decision.effectiveResolutionStatus !== undefined
84
127
  && decision.persistedResolutionStatus
85
128
  !== decision.effectiveResolutionStatus))
@@ -96,6 +139,29 @@ function compactCount(value: unknown): number {
96
139
  ? Math.max(0, Math.floor(value)) : 0;
97
140
  }
98
141
 
142
+ function normalizedReferenceValues(
143
+ values: readonly string[] | undefined,
144
+ isSafe: (value: string) => boolean,
145
+ ): Map<string, boolean> {
146
+ const unique = new Map<string, boolean>();
147
+ for (const raw of values ?? []) {
148
+ const value = raw.trim();
149
+ if (!value) continue;
150
+ unique.set(value, (unique.get(value) ?? true) && isSafe(raw));
151
+ }
152
+ return unique;
153
+ }
154
+
155
+ function safeReferenceText(value: string, maxLength: number): boolean {
156
+ if (hasControlCharacter(value)) return false;
157
+ const normalized = value.trim();
158
+ return normalized.length > 0
159
+ && normalized.length <= maxLength
160
+ && !/^[a-z]+:\/\//i.test(normalized)
161
+ && !/(?:^|[^A-Za-z0-9])(?:authorization|bearer|credential|destination|password|secret|token)(?:$|[^A-Za-z0-9])/i
162
+ .test(normalized);
163
+ }
164
+
99
165
  function hasControlCharacter(value: string): boolean {
100
166
  for (let index = 0; index < value.length; index += 1) {
101
167
  const code = value.charCodeAt(index);
@@ -10,6 +10,7 @@ import { normalizeCompactDecisionEquivalence } from
10
10
  export interface CompactDecisionNode {
11
11
  key?: string;
12
12
  decisionTarget?: string;
13
+ projectedIdentity?: boolean;
13
14
  }
14
15
 
15
16
  function resolvedNode(
@@ -40,7 +41,10 @@ function sameCanonicalNode(
40
41
  left: CompactDecisionNode | undefined,
41
42
  right: CompactDecisionNode,
42
43
  ): boolean {
43
- return left?.key !== undefined && left.key === right.key;
44
+ if (left?.key === undefined) return false;
45
+ return left.projectedIdentity === true
46
+ ? left.key === right.decisionTarget
47
+ : left.key === right.key;
44
48
  }
45
49
 
46
50
  function persistedCanonical(
@@ -49,7 +53,7 @@ function persistedCanonical(
49
53
  target: CompactDecisionNode,
50
54
  ): boolean {
51
55
  if (!sameCanonicalNode(persisted, target)) return false;
52
- return effective?.key === undefined || effective.key === target.key;
56
+ return effective?.key === undefined || sameCanonicalNode(effective, target);
53
57
  }
54
58
 
55
59
  export function projectObservationDecision(
@@ -71,6 +75,7 @@ export function projectObservationDecision(
71
75
  target.decisionTarget,
72
76
  sameCanonicalNode(effective, target),
73
77
  persistedCanonical(persisted, effective, target),
78
+ source?.persistedResolutionStatus,
74
79
  );
75
80
  return decision;
76
81
  }
@@ -150,6 +150,7 @@ function unresolvedOperationStart(
150
150
  severity: 'warning',
151
151
  code: 'trace_start_implementation_unresolved',
152
152
  message: 'Indexed operation matched but no implementation candidate exists',
153
+ selectorKind: 'operation',
153
154
  resolutionStage: 'implementation',
154
155
  resolutionStatus: 'operation_without_implementation',
155
156
  }],
@@ -17,10 +17,7 @@ import {
17
17
  TraversalScopeScheduler,
18
18
  type TraversalScopeState,
19
19
  } from './010-traversal-scope.js';
20
- import {
21
- planEventSubscriberTransitions,
22
- type PlannedEventSubscriberTransition,
23
- } from './011-event-subscriber-traversal.js';
20
+ import type { PlannedEventSubscriberTransition } from './011-event-subscriber-traversal.js';
24
21
  import {
25
22
  graphForCalls,
26
23
  operationNode,
@@ -36,7 +33,6 @@ import {
36
33
  import type { CompactSemanticEndpoint } from './014-compact-contract.js';
37
34
  import { TraceEdgeRecorder } from './015-trace-edge-recorder.js';
38
35
  import {
39
- contextForSymbolCall,
40
36
  knownBindingsForCalls,
41
37
  knownBindingsForScope,
42
38
  parseTraceEvidence,
@@ -53,6 +49,8 @@ import {
53
49
  import { outboundScopeSymbolIds } from './023-nested-event-scopes.js';
54
50
  import type { ImplementationHintOptions } from './025-trace-implementation-scope.js';
55
51
  import { processOperationTarget } from './028-trace-operation-execution.js';
52
+ import { runtimeEventResolution, runtimeEventSubscriberPlans } from './030-event-runtime-resolution.js';
53
+ import { planLocalCallExpansion } from './031-local-call-expansion.js';
56
54
 
57
55
  export interface CallRow extends Record<string, unknown> {
58
56
  id: number;
@@ -226,20 +224,10 @@ function processLocalCall(
226
224
  bindings: Map<string, ContextBinding>,
227
225
  row: Record<string, unknown>,
228
226
  ): void {
229
- if (!row.callee_symbol_id) return;
230
- const symbolId = Number(row.callee_symbol_id);
231
- const symbols = new Set([symbolId]);
232
- const files = new Set([String(row.calleeFile)]);
233
- const repoId = Number(row.calleeRepoId);
234
- const context = contextForSymbolCall(runtime.db, row, bindings);
235
- const scheduling = runtime.scheduler.schedule({
236
- workspaceId: runtime.workspaceId,
237
- repoId,
238
- files,
239
- symbolIds: symbols,
240
- context,
241
- }, current.state);
242
- const node = symbolNode(runtime.db, symbolId);
227
+ const symbolId = row.callee_symbol_id
228
+ ? Number(row.callee_symbol_id) : undefined;
229
+ const node = symbolId === undefined
230
+ ? undefined : symbolNode(runtime.db, symbolId);
243
231
  if (node) runtime.nodes.set(String(node.id), node);
244
232
  const evidence = localCallEvidence(row, node);
245
233
  const unresolvedReason = localCallUnresolvedReason(row);
@@ -247,6 +235,12 @@ function processLocalCall(
247
235
  const target = recordLocalCallObservation(runtime.recorder, edge, {
248
236
  symbolCall: row, evidence, unresolvedReason,
249
237
  });
238
+ if (symbolId === undefined || row.status !== 'resolved' || !node) return;
239
+ const { repoId, files, symbols, context, scheduling } =
240
+ planLocalCallExpansion(
241
+ runtime.db, runtime.scheduler, runtime.workspaceId, current.state,
242
+ row, bindings, symbolId,
243
+ );
250
244
  if (scheduling.kind === 'cycle')
251
245
  recordLocalCycle(runtime, current, row, target, scheduling.state,
252
246
  repoId, files, symbols);
@@ -291,7 +285,8 @@ function localTraceEdge(
291
285
  type: 'local_symbol_call',
292
286
  from: String(row.callee_expression),
293
287
  to: node?.label
294
- ? String(node.label) : `symbol:${String(row.callee_symbol_id)}`,
288
+ ? String(node.label)
289
+ : `${String(row.status)}:${String(row.callee_expression)}`,
295
290
  evidence,
296
291
  confidence: Number(row.confidence ?? 0.8),
297
292
  unresolvedReason,
@@ -390,11 +385,12 @@ function recordEffectiveOutbound(
390
385
  ): EffectiveOutbound {
391
386
  const persisted = parseTraceEvidence(row.evidence_json);
392
387
  const raw = baseTraceEvidence(row, call, persisted, contextual.evidence);
393
- const effective = runtimeResolution(runtime.db, row, raw, {
394
- vars: runtime.options.vars,
395
- dynamicMode: runtime.options.dynamicMode ?? 'strict',
396
- maxDynamicCandidates: runtime.options.maxDynamicCandidates,
397
- }, call.workspaceId, contextual.state);
388
+ const effective = runtimeEventResolution(row, raw, runtime.options.vars)
389
+ ?? runtimeResolution(runtime.db, row, raw, {
390
+ vars: runtime.options.vars,
391
+ dynamicMode: runtime.options.dynamicMode ?? 'strict',
392
+ maxDynamicCandidates: runtime.options.maxDynamicCandidates,
393
+ }, call.workspaceId, contextual.state);
398
394
  const target = `${effective.row.to_kind}:${effective.row.to_id}`;
399
395
  const operation = effective.row.to_kind === 'operation'
400
396
  ? operationNode(runtime.db, effective.row.to_id) : undefined;
@@ -461,15 +457,19 @@ function processEventTransitions(
461
457
  ): void {
462
458
  if (!runtime.options.includeAsync || call.call_type !== 'async_emit'
463
459
  || effective.row.edge_type !== 'HANDLER_EMITS_EVENT'
464
- || typeof call.event_name_expr !== 'string') return;
465
- const plans = planEventSubscriberTransitions(runtime.db, {
466
- workspaceId: runtime.workspaceId ?? call.workspaceId,
467
- graphGeneration: call.graphGeneration,
468
- eventName: call.event_name_expr,
469
- }, runtime.scheduler, current.state, current.depth, runtime.maxDepth);
470
- for (const plan of plans)
460
+ || effective.row.to_kind !== 'event') return;
461
+ const planned = runtimeEventSubscriberPlans(
462
+ runtime.db, {
463
+ workspaceId: runtime.workspaceId ?? call.workspaceId,
464
+ graphGeneration: call.graphGeneration,
465
+ eventName: effective.row.to_id,
466
+ vars: runtime.options.vars ?? {},
467
+ }, runtime.scheduler, current.state, current.depth, runtime.maxDepth,
468
+ );
469
+ if (planned.diagnostic) runtime.diagnostics.push(planned.diagnostic);
470
+ for (const plan of planned.plans)
471
471
  recordEventTransition(runtime, current, plan,
472
- effective.semanticWorkspaceId, plans.length);
472
+ effective.semanticWorkspaceId, planned.plans.length);
473
473
  }
474
474
 
475
475
  function recordEventTransition(
@@ -0,0 +1,151 @@
1
+ import {
2
+ substituteVariables,
3
+ type RuntimeSubstitution,
4
+ } from '../linker/dynamic-edge-resolver.js';
5
+ import type { Db } from '../db/connection.js';
6
+ import {
7
+ eventSubscriberMissingVariables,
8
+ planEventSubscriberTransitions,
9
+ type EventSubscriberTransitionQuery,
10
+ type PlannedEventSubscriberTransition,
11
+ } from './011-event-subscriber-traversal.js';
12
+ import type {
13
+ TraversalScopeScheduler,
14
+ TraversalScopeState,
15
+ } from './010-traversal-scope.js';
16
+ import type { TraceGraphRow } from './evidence.js';
17
+
18
+ export interface EventRuntimeResolution {
19
+ row: TraceGraphRow;
20
+ evidence: Record<string, unknown>;
21
+ unresolvedReason?: string;
22
+ }
23
+
24
+ export interface EventSubscriberRuntimePlan {
25
+ plans: PlannedEventSubscriberTransition[];
26
+ diagnostic?: Record<string, unknown>;
27
+ }
28
+
29
+ function record(value: unknown): Record<string, unknown> | undefined {
30
+ return value && typeof value === 'object' && !Array.isArray(value)
31
+ ? value as Record<string, unknown> : undefined;
32
+ }
33
+
34
+ function eventTemplate(evidence: Record<string, unknown>): string | undefined {
35
+ const resolution = record(evidence.eventTemplateResolution);
36
+ return typeof resolution?.original === 'string'
37
+ ? resolution.original : undefined;
38
+ }
39
+
40
+ function missingReason(substitution: RuntimeSubstitution): string {
41
+ return `Dynamic target requires runtime variable overrides: ${
42
+ substitution.missing.join(', ')}`;
43
+ }
44
+
45
+ function missingSubscriberDiagnostic(
46
+ missing: readonly string[],
47
+ ): Record<string, unknown> | undefined {
48
+ if (missing.length === 0) return undefined;
49
+ return {
50
+ severity: 'warning',
51
+ code: 'trace_runtime_variables_missing',
52
+ message: `Runtime variables are required to resolve dynamic event subscribers: ${
53
+ missing.join(', ')}`,
54
+ missingVariables: missing,
55
+ suggestions: missing.map((key) => `--var ${key}=<value>`),
56
+ source: 'event_subscription',
57
+ };
58
+ }
59
+
60
+ export function runtimeEventSubscriberPlans(
61
+ db: Db,
62
+ query: EventSubscriberTransitionQuery,
63
+ scheduler: TraversalScopeScheduler,
64
+ parent: TraversalScopeState,
65
+ depth: number,
66
+ maxDepth: number,
67
+ ): EventSubscriberRuntimePlan {
68
+ const missing = eventSubscriberMissingVariables(db, query);
69
+ return {
70
+ plans: planEventSubscriberTransitions(
71
+ db, query, scheduler, parent, depth, maxDepth,
72
+ ),
73
+ diagnostic: missingSubscriberDiagnostic(missing),
74
+ };
75
+ }
76
+
77
+ function eventEvidence(
78
+ evidence: Record<string, unknown>,
79
+ substitution: RuntimeSubstitution,
80
+ row: TraceGraphRow,
81
+ unresolvedReason?: string,
82
+ ): Record<string, unknown> {
83
+ const effectiveResolution = {
84
+ status: unresolvedReason ? 'dynamic' : 'terminal',
85
+ targetKind: row.to_kind,
86
+ targetId: row.to_id,
87
+ confidence: row.confidence,
88
+ unresolvedReason,
89
+ edgeType: row.edge_type,
90
+ };
91
+ return {
92
+ ...evidence,
93
+ runtimeSubstitutions: { eventName: substitution },
94
+ suppliedRuntimeVariables: Object.fromEntries(
95
+ substitution.supplied.map((key) => [key, true]),
96
+ ),
97
+ ...(substitution.supplied.length > 0
98
+ ? { runtimeVariablesApplied: true } : {}),
99
+ ...(substitution.missing.length > 0
100
+ ? { missingRuntimeVariables: substitution.missing } : {}),
101
+ effectiveResolution,
102
+ linker: {
103
+ status: effectiveResolution.status,
104
+ confidence: row.confidence,
105
+ reason: unresolvedReason,
106
+ edgeType: row.edge_type,
107
+ },
108
+ };
109
+ }
110
+
111
+ function runtimeEventRow(
112
+ row: TraceGraphRow,
113
+ evidence: Record<string, unknown>,
114
+ substitution: RuntimeSubstitution,
115
+ template: string,
116
+ ): TraceGraphRow {
117
+ const missing = substitution.missing.length > 0;
118
+ const eventName = substitution.effective ?? template;
119
+ const callType = evidence.callType;
120
+ const edgeType = callType === 'async_emit'
121
+ ? 'HANDLER_EMITS_EVENT' : 'EVENT_CONSUMED_BY_HANDLER';
122
+ return {
123
+ ...row,
124
+ edge_type: missing ? 'DYNAMIC_EDGE_CANDIDATE' : edgeType,
125
+ status: missing ? 'dynamic' : 'terminal',
126
+ to_kind: missing ? 'event_candidate' : 'event',
127
+ to_id: missing ? `Event: ${eventName}` : eventName,
128
+ unresolved_reason: missing ? missingReason(substitution) : undefined,
129
+ };
130
+ }
131
+
132
+ export function runtimeEventResolution(
133
+ row: TraceGraphRow,
134
+ evidence: Record<string, unknown>,
135
+ variables: Record<string, string> | undefined,
136
+ ): EventRuntimeResolution | undefined {
137
+ const template = eventTemplate(evidence);
138
+ if (!template || row.to_kind === 'event' && variables === undefined)
139
+ return undefined;
140
+ const substitution = substituteVariables(template, variables ?? {});
141
+ const effectiveRow = runtimeEventRow(
142
+ row, evidence, substitution, template,
143
+ );
144
+ return {
145
+ row: effectiveRow,
146
+ evidence: eventEvidence(
147
+ evidence, substitution, effectiveRow, effectiveRow.unresolved_reason,
148
+ ),
149
+ unresolvedReason: effectiveRow.unresolved_reason,
150
+ };
151
+ }
@@ -0,0 +1,37 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ type ContextBinding,
4
+ } from './008-contextual-runtime-state.js';
5
+ import {
6
+ type TraversalScheduleDecision,
7
+ type TraversalScopeScheduler,
8
+ type TraversalScopeState,
9
+ } from './010-traversal-scope.js';
10
+ import { contextForSymbolCall } from './017-trace-context.js';
11
+
12
+ export interface LocalCallExpansion {
13
+ repoId: number;
14
+ files: Set<string>;
15
+ symbols: Set<number>;
16
+ context: Map<string, ContextBinding>;
17
+ scheduling: TraversalScheduleDecision;
18
+ }
19
+
20
+ export function planLocalCallExpansion(
21
+ db: Db,
22
+ scheduler: TraversalScopeScheduler,
23
+ workspaceId: number | undefined,
24
+ parent: TraversalScopeState,
25
+ row: Record<string, unknown>,
26
+ bindings: Map<string, ContextBinding>,
27
+ symbolId: number,
28
+ ): LocalCallExpansion {
29
+ const symbols = new Set([symbolId]);
30
+ const files = new Set([String(row.calleeFile)]);
31
+ const repoId = Number(row.calleeRepoId);
32
+ const context = contextForSymbolCall(db, row, bindings);
33
+ const scheduling = scheduler.schedule({
34
+ workspaceId, repoId, files, symbolIds: symbols, context,
35
+ }, parent);
36
+ return { repoId, files, symbols, context, scheduling };
37
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ImplementationHint } from '../types.js';
2
2
  import { projectBounded } from '../utils/000-bounded-projection.js';
3
+ import { compareBinary } from './010-traversal-scope.js';
3
4
 
4
5
  interface Candidate {
5
6
  accepted?: boolean;
@@ -106,6 +107,7 @@ export function implementationHintSuggestions(rawEvidence: Record<string, unknow
106
107
 
107
108
  export function implementationHintSuggestionProjection(
108
109
  rawEvidence: Record<string, unknown>,
110
+ limit?: number,
109
111
  ): ImplementationHintSuggestionProjection {
110
112
  const evidence = asEvidence(rawEvidence);
111
113
  const accepted = (evidence.candidates ?? []).filter((candidate) => candidate.accepted);
@@ -119,7 +121,7 @@ export function implementationHintSuggestionProjection(
119
121
  }
120
122
  const repos = selectableRepositories(accepted);
121
123
  const repositoryProjection = projectBounded(
122
- repos, (left, right) => left.localeCompare(right),
124
+ repos, compareBinary, limit,
123
125
  );
124
126
  const suggestions = accepted
125
127
  .flatMap((candidate) => {
@@ -142,7 +144,7 @@ export function implementationHintSuggestionProjection(
142
144
  cli: `--implementation-hint ${hintString(hint)}`,
143
145
  }];
144
146
  });
145
- const projection = projectBounded(suggestions, compareSuggestion);
147
+ const projection = projectBounded(suggestions, compareSuggestion, limit);
146
148
  return {
147
149
  suggestions: projection.items,
148
150
  suggestionCount: projection.totalCount,
@@ -170,15 +172,16 @@ function projectedSuggestions(value: unknown): ImplementationHintSuggestionProje
170
172
  }
171
173
 
172
174
  function compareHints(left: ImplementationHint, right: ImplementationHint): number {
173
- return hintString(left).localeCompare(hintString(right));
175
+ return compareBinary(hintString(left), hintString(right));
174
176
  }
175
177
 
176
178
  function compareSuggestion(
177
179
  left: Record<string, unknown>,
178
180
  right: Record<string, unknown>,
179
181
  ): number {
180
- return String(left.cli ?? '').localeCompare(String(right.cli ?? ''))
181
- || String(left.implementationRepo ?? '').localeCompare(
182
+ return compareBinary(String(left.cli ?? ''), String(right.cli ?? ''))
183
+ || compareBinary(
184
+ String(left.implementationRepo ?? ''),
182
185
  String(right.implementationRepo ?? ''),
183
186
  );
184
187
  }
@@ -202,7 +205,7 @@ function selectableRepositories(candidates: Candidate[]): string[] {
202
205
  const repos = new Set(candidates.flatMap((candidate) => candidate.handlerPackage?.name ? [candidate.handlerPackage.name] : []));
203
206
  return [...repos]
204
207
  .filter((repo) => candidates.filter((candidate) => candidateMatchesRepo(candidate, repo)).length === 1)
205
- .sort();
208
+ .sort(compareBinary);
206
209
  }
207
210
 
208
211
  function assignHintField(hint: Partial<ImplementationHint>, key: string, value: string): void {
@@ -550,6 +550,7 @@ export function ambiguousStartDiagnostic(
550
550
  severity: 'warning',
551
551
  code: 'trace_start_ambiguous',
552
552
  message,
553
+ selectorKind: 'operation',
553
554
  normalizedSelectorValue: requested,
554
555
  resolutionStage: 'operation',
555
556
  resolutionStatus: 'ambiguous_operation',
package/src/types.ts CHANGED
@@ -203,7 +203,8 @@ export interface ServiceBindingReference {
203
203
  bindingSiteEndOffset?: number;
204
204
  resolutionStrategy?: 'lexical_declaration'
205
205
  | 'lexical_alias_declaration'
206
- | 'deterministic_reaching_assignment';
206
+ | 'deterministic_reaching_assignment'
207
+ | 'single_hop_helper_return';
207
208
  lexicalScopeChain?: LexicalScopeFact[];
208
209
  bindingScopeIndex?: number;
209
210
  scopeChainTotal: number;
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import packageJson from '../package.json' with { type: 'json' };
2
2
 
3
3
  export const VERSION = packageJson.version;
4
- export const ANALYZER_VERSION = '0.1.68-facts.1';
4
+ export const ANALYZER_VERSION = '0.1.69-facts.1';