@saptools/service-flow 0.1.68 → 0.1.70

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 (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +13 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-GSLFY6J2.js} +6986 -5503
  5. package/dist/chunk-GSLFY6J2.js.map +1 -0
  6. package/dist/cli.js +334 -122
  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/{002-doctor-lifecycle.ts → doctor-lifecycle.ts} +1 -1
  12. package/src/cli/{001-doctor-projection.ts → doctor-projection.ts} +1 -1
  13. package/src/cli/doctor.ts +5 -5
  14. package/src/cli/index-summary.ts +22 -0
  15. package/src/cli.ts +155 -91
  16. package/src/db/{009-binding-fact-semantics.ts → binding-fact-semantics.ts} +6 -1
  17. package/src/db/binding-helper-provenance.ts +17 -0
  18. package/src/db/{012-binding-reference-proof.ts → binding-reference-proof.ts} +4 -0
  19. package/src/db/{000-call-fact-repository.ts → call-fact-repository.ts} +49 -23
  20. package/src/db/{003-current-fact-semantics.ts → current-fact-semantics.ts} +5 -4
  21. package/src/db/{001-fact-lifecycle.ts → fact-lifecycle.ts} +3 -3
  22. package/src/db/index-publication-failure.ts +91 -0
  23. package/src/db/{007-package-fact-semantics.ts → package-fact-semantics.ts} +6 -6
  24. package/src/db/{010-package-symbol-surface-semantics.ts → package-symbol-surface-semantics.ts} +2 -2
  25. package/src/db/{004-package-target-invalidation.ts → package-target-invalidation.ts} +11 -1
  26. package/src/db/{008-relative-fact-semantics.ts → relative-fact-semantics.ts} +9 -6
  27. package/src/db/{006-relative-symbol-resolution.ts → relative-symbol-resolution.ts} +28 -7
  28. package/src/db/repositories.ts +24 -7
  29. package/src/db/{011-symbol-call-semantics.ts → symbol-call-semantics.ts} +1 -1
  30. package/src/index.ts +3 -3
  31. package/src/indexer/cds-extension-resolver.ts +27 -3
  32. package/src/indexer/repository-indexer.ts +70 -9
  33. package/src/indexer/workspace-indexer.ts +142 -30
  34. package/src/linker/call-edge-insertion.ts +568 -0
  35. package/src/linker/{002-call-evidence.ts → call-evidence.ts} +2 -2
  36. package/src/linker/cross-repo-linker.ts +6 -169
  37. package/src/linker/dynamic-edge-resolver.ts +1 -1
  38. package/src/linker/{004-event-subscription-handler-linker.ts → event-subscription-handler-linker.ts} +32 -11
  39. package/src/linker/event-template-link.ts +72 -0
  40. package/src/linker/external-http-target.ts +1 -1
  41. package/src/linker/helper-package-linker.ts +1 -1
  42. package/src/linker/{000-implementation-candidates.ts → implementation-candidates.ts} +1 -1
  43. package/src/linker/{001-implementation-evidence-projection.ts → implementation-evidence-projection.ts} +1 -1
  44. package/src/linker/odata-path-normalizer.ts +1 -1
  45. package/src/linker/{005-odata-path-structure.ts → odata-path-structure.ts} +1 -1
  46. package/src/linker/{003-package-import-symbol-resolver.ts → package-import-symbol-resolver.ts} +3 -3
  47. package/src/linker/service-resolver.ts +2 -2
  48. package/src/output/{001-compact-json-output.ts → compact-json-output.ts} +1 -1
  49. package/src/parsers/{006-binding-identity.ts → binding-identity.ts} +9 -4
  50. package/src/parsers/{011-binding-lexical-scope.ts → binding-lexical-scope.ts} +1 -1
  51. package/src/parsers/{021-binding-visibility.ts → binding-visibility.ts} +18 -2
  52. package/src/parsers/{000-direct-query-execution.ts → direct-query-execution.ts} +11 -0
  53. package/src/parsers/{005-event-subscription-facts.ts → event-subscription-facts.ts} +2 -2
  54. package/src/parsers/{016-local-symbol-reference.ts → local-symbol-reference.ts} +2 -2
  55. package/src/parsers/operation-path-analysis.ts +1 -1
  56. package/src/parsers/outbound-call-classifier.ts +692 -0
  57. package/src/parsers/outbound-call-parser.ts +146 -509
  58. package/src/parsers/outbound-expression-analysis.ts +700 -0
  59. package/src/parsers/{018-package-commonjs-syntax.ts → package-commonjs-syntax.ts} +1 -1
  60. package/src/parsers/{012-package-fact-contract.ts → package-fact-contract.ts} +3 -3
  61. package/src/parsers/{010-package-public-surface-analysis.ts → package-public-surface-analysis.ts} +2 -2
  62. package/src/parsers/{003-package-public-surface.ts → package-public-surface.ts} +3 -3
  63. package/src/parsers/{008-package-surface-publication.ts → package-surface-publication.ts} +1 -1
  64. package/src/parsers/{001-query-entity-resolution.ts → query-entity-resolution.ts} +1 -1
  65. package/src/parsers/{015-service-binding-collector.ts → service-binding-collector.ts} +3 -3
  66. package/src/parsers/{014-service-binding-helper-flow.ts → service-binding-helper-flow.ts} +72 -6
  67. package/src/parsers/service-binding-parser-helpers.ts +1 -1
  68. package/src/parsers/service-binding-parser.ts +2 -2
  69. package/src/parsers/{007-source-fact-reconciliation.ts → source-fact-reconciliation.ts} +3 -3
  70. package/src/parsers/{020-stable-local-value.ts → stable-local-value.ts} +2 -2
  71. package/src/parsers/{009-symbol-call-facts.ts → symbol-call-facts.ts} +3 -3
  72. package/src/parsers/{017-symbol-derived-contexts.ts → symbol-derived-contexts.ts} +2 -2
  73. package/src/parsers/symbol-parser.ts +43 -11
  74. package/src/trace/{014-compact-contract.ts → compact-contract.ts} +6 -0
  75. package/src/trace/{021-compact-decision-normalization.ts → compact-decision-normalization.ts} +77 -11
  76. package/src/trace/{020-compact-field-projection.ts → compact-field-projection.ts} +77 -4
  77. package/src/trace/{024-compact-observation-decision.ts → compact-observation-decision.ts} +10 -5
  78. package/src/trace/{016-compact-projector.ts → compact-projector.ts} +6 -7
  79. package/src/trace/{018-compact-trace.ts → compact-trace.ts} +3 -3
  80. package/src/trace/{006-contextual-projection.ts → contextual-projection.ts} +1 -1
  81. package/src/trace/{008-contextual-runtime-state.ts → contextual-runtime-state.ts} +1 -1
  82. package/src/trace/{001-dynamic-identity.ts → dynamic-identity.ts} +2 -2
  83. package/src/trace/{003-dynamic-references.ts → dynamic-references.ts} +2 -2
  84. package/src/trace/dynamic-targets.ts +6 -6
  85. package/src/trace/event-runtime-resolution.ts +151 -0
  86. package/src/trace/{011-event-subscriber-traversal.ts → event-subscriber-traversal.ts} +101 -9
  87. package/src/trace/evidence.ts +2 -2
  88. package/src/trace/implementation-hints.ts +10 -7
  89. package/src/trace/{005-implementation-selection.ts → implementation-selection.ts} +2 -2
  90. package/src/trace/{007-implementation-start-diagnostic.ts → implementation-start-diagnostic.ts} +1 -0
  91. package/src/trace/local-call-expansion.ts +37 -0
  92. package/src/trace/{009-selected-handler-provenance.ts → selected-handler-provenance.ts} +1 -1
  93. package/src/trace/selectors.ts +2 -1
  94. package/src/trace/{017-trace-context.ts → trace-context.ts} +2 -2
  95. package/src/trace/{015-trace-edge-recorder.ts → trace-edge-recorder.ts} +62 -5
  96. package/src/trace/{019-trace-edge-semantics.ts → trace-edge-semantics.ts} +9 -13
  97. package/src/trace/trace-engine.ts +10 -10
  98. package/src/trace/{022-trace-fact-preflight.ts → trace-fact-preflight.ts} +2 -2
  99. package/src/trace/{025-trace-implementation-scope.ts → trace-implementation-scope.ts} +1 -1
  100. package/src/trace/{028-trace-operation-execution.ts → trace-operation-execution.ts} +10 -10
  101. package/src/trace/{013-trace-root-scopes.ts → trace-root-scopes.ts} +5 -6
  102. package/src/trace/{027-trace-scope-execution.ts → trace-scope-execution.ts} +44 -44
  103. package/src/trace/{029-trace-start-implementation.ts → trace-start-implementation.ts} +7 -7
  104. package/src/trace/{026-trace-start-scope.ts → trace-start-scope.ts} +6 -5
  105. package/src/trace/{010-traversal-scope.ts → traversal-scope.ts} +1 -1
  106. package/src/types.ts +2 -1
  107. package/src/version.ts +1 -1
  108. package/dist/chunk-AEM4JY22.js.map +0 -1
  109. /package/src/cli/{000-clean.ts → clean.ts} +0 -0
  110. /package/src/cli/{003-doctor-package-resolution.ts → doctor-package-resolution.ts} +0 -0
  111. /package/src/db/{002-fact-json-inventory.ts → fact-json-inventory.ts} +0 -0
  112. /package/src/db/{005-schema-structure.ts → schema-structure.ts} +0 -0
  113. /package/src/output/{000-stdout-policy.ts → stdout-policy.ts} +0 -0
  114. /package/src/parsers/{019-binding-assignment-targets.ts → binding-assignment-targets.ts} +0 -0
  115. /package/src/parsers/{013-executable-body-eligibility.ts → executable-body-eligibility.ts} +0 -0
  116. /package/src/parsers/{004-fact-identity.ts → fact-identity.ts} +0 -0
  117. /package/src/parsers/{002-symbol-import-bindings.ts → symbol-import-bindings.ts} +0 -0
  118. /package/src/trace/{004-dynamic-candidate-sources.ts → dynamic-candidate-sources.ts} +0 -0
  119. /package/src/trace/{000-dynamic-target-types.ts → dynamic-target-types.ts} +0 -0
  120. /package/src/trace/{023-nested-event-scopes.ts → nested-event-scopes.ts} +0 -0
  121. /package/src/trace/{002-trace-diagnostics.ts → trace-diagnostics.ts} +0 -0
  122. /package/src/trace/{012-trace-graph-lookups.ts → trace-graph-lookups.ts} +0 -0
  123. /package/src/utils/{000-bounded-projection.ts → bounded-projection.ts} +0 -0
  124. /package/src/utils/{001-placeholders.ts → placeholders.ts} +0 -0
@@ -1,20 +1,10 @@
1
1
  import type { Db } from '../db/connection.js';
2
- import { applyVariables } from './dynamic-edge-resolver.js';
3
- import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from './odata-path-normalizer.js';
4
- import { buildRemoteQueryTarget } from './remote-query-target.js';
5
- import { resolveOperation } from './service-resolver.js';
6
2
  import { linkHelperPackages } from './helper-package-linker.js';
7
- import { externalHttpTarget } from './external-http-target.js';
8
- import { linkImplementations as linkCanonicalImplementations } from './000-implementation-candidates.js';
9
- import {
10
- ambiguousPathCandidates,
11
- linkedCallEvidence,
12
- objectJson,
13
- objectValue,
14
- } from './002-call-evidence.js';
15
- import { linkPackageImportSymbolCalls } from './003-package-import-symbol-resolver.js';
16
- import { linkEventSubscriptionHandlers } from './004-event-subscription-handler-linker.js';
17
- import { assertWorkspaceLinkable } from '../db/001-fact-lifecycle.js';
3
+ import { linkImplementations as linkCanonicalImplementations } from './implementation-candidates.js';
4
+ import { linkPackageImportSymbolCalls } from './package-import-symbol-resolver.js';
5
+ import { linkEventSubscriptionHandlers } from './event-subscription-handler-linker.js';
6
+ import { insertCallEdge } from './call-edge-insertion.js';
7
+ import { assertWorkspaceLinkable } from '../db/fact-lifecycle.js';
18
8
  export interface LinkWorkspaceResult {
19
9
  edgeCount: number;
20
10
  unresolvedCount: number;
@@ -43,7 +33,7 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
43
33
  db.prepare('DELETE FROM graph_edges WHERE workspace_id=?').run(workspaceId);
44
34
  const deps = linkHelperPackages(db, workspaceId, generation);
45
35
  const subscriptions = linkEventSubscriptionHandlers(
46
- db, workspaceId, generation,
36
+ db, workspaceId, generation, vars,
47
37
  );
48
38
  const impl = linkCanonicalImplementations(db, workspaceId, generation);
49
39
  const callSummary = linkCalls(db, workspaceId, vars, generation);
@@ -89,156 +79,3 @@ function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, ge
89
79
  }
90
80
  return { edgeCount, unresolvedCount, resolvedCount, remoteResolvedCount, localResolvedCount, ambiguousCount, dynamicCount, terminalCount };
91
81
  }
92
- function insertCallEdge(db: Db, workspaceId: number, call: Record<string, unknown>, vars: Record<string, string>, generation: number): { status: string; callType: string } {
93
- const callType = String(call.call_type);
94
- const rawOp = applyVariables(String(call.operation_path_expr ?? ''), vars);
95
- const intent = classifyODataPathIntent(rawOp, call.method as string | undefined);
96
- const isEntityQueryIntent = ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
97
- const resolutionRawOp = callType === 'remote_query' && isEntityQueryIntent ? intent.pathWithoutQuery : rawOp;
98
- const normalized = normalizeODataOperationInvocationPath(resolutionRawOp);
99
- const op = normalized?.normalizedOperationPath ?? resolutionRawOp;
100
- const servicePath = applyVariables((call.servicePathExpr as string | undefined) ?? (call.requireServicePath as string | undefined), vars);
101
- const destination = (call.destinationExpr as string | undefined) ?? (call.requireDestination as string | undefined);
102
- const isDynamic = Boolean(Number(call.isDynamic ?? 0));
103
- const isRemoteEntityCall = callType.startsWith('remote_entity_');
104
- const indexedOperationCandidateCount = operationCandidateCount(db, workspaceId, op, intent.topLevelOperationName);
105
- const credibleOperationSignal = Boolean(normalized?.wasInvocation) || (Boolean(intent.topLevelOperationNameCandidate) && indexedOperationCandidateCount > 0);
106
- const strongEntitySignal = ['entity_media', 'entity_delete', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind) || (intent.kind === 'entity_mutation' && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix));
107
- const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && credibleOperationSignal && (!strongEntitySignal || indexedOperationCandidateCount > 0);
108
- const isOperationCall = operationLikeRemoteEntity || ((callType === 'remote_action' || callType === 'local_service_call') || (callType === 'remote_query' && Boolean(op)));
109
- const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name as string | undefined, repoId: callType === 'local_service_call' ? Number(call.repo_id) : undefined, alias: applyVariables((call.aliasExpr as string | undefined) ?? (call.alias as string | undefined), vars), destination: destination ? applyVariables(destination, vars) : undefined, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === 'local_service_call' }, workspaceId) : { status: 'unresolved' as const, candidates: [], reasons: [] };
110
- const evidence: Record<string, unknown> = {
111
- ...linkedCallEvidence(
112
- call,
113
- resolution,
114
- servicePath,
115
- op,
116
- destination ? applyVariables(destination, vars) : undefined,
117
- normalized,
118
- intent,
119
- ),
120
- indexedOperationCandidateCount,
121
- parserCallType: callType,
122
- entityOperationPrecedence: operationPrecedence(
123
- callType,
124
- intent,
125
- indexedOperationCandidateCount,
126
- Boolean(resolution.target),
127
- ),
128
- };
129
- const pathAnalysis = objectValue(objectJson(call.evidence_json)?.pathAnalysis);
130
- if (callType === 'remote_action' && pathAnalysis?.status === 'ambiguous') {
131
- const candidatePaths = ambiguousPathCandidates(pathAnalysis);
132
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(
133
- workspaceId,
134
- 'UNRESOLVED_EDGE',
135
- 'ambiguous',
136
- 'call',
137
- String(call.id),
138
- 'operation_candidates',
139
- candidatePaths.items.join(','),
140
- Number(call.confidence ?? 0.5),
141
- JSON.stringify({
142
- ...evidence,
143
- ambiguousOperationPathCandidateCount: candidatePaths.totalCount,
144
- shownAmbiguousOperationPathCandidateCount: candidatePaths.shownCount,
145
- omittedAmbiguousOperationPathCandidateCount: candidatePaths.omittedCount,
146
- }),
147
- 0,
148
- 'Ambiguous operation path candidates require explicit disambiguation',
149
- generation,
150
- );
151
- return { status: 'ambiguous', callType };
152
- }
153
- if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === 'dynamic')) {
154
- if (resolution.target) {
155
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'REMOTE_CALL_RESOLVES_TO_OPERATION', 'resolved', 'call', String(call.id), 'operation', String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: 'indexed_operation_over_parser_entity' }), 0, generation);
156
- return { status: 'resolved', callType };
157
- }
158
- const status = resolution.status === 'dynamic' ? 'dynamic' : resolution.status === 'ambiguous' ? 'ambiguous' : 'unresolved';
159
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, status === 'dynamic' ? 'DYNAMIC_EDGE_CANDIDATE' : 'UNRESOLVED_EDGE', status, 'call', String(call.id), 'operation_candidate', op ? `Remote action: ${op}` : 'Remote action: unknown path', Number(call.confidence ?? 0.2), JSON.stringify({ ...evidence, operationEntityPrecedence: resolution.candidates.length > 0 ? 'parser_entity_with_indexed_operation_candidates' : 'parser_entity_operation_candidate_without_indexed_match' }), status === 'dynamic' ? 1 : 0, unresolvedOperationReason(resolution), generation);
160
- return { status, callType };
161
- }
162
- if (isRemoteEntityCall) {
163
- const target = buildRemoteQueryTarget({ queryEntity: intent.entitySegment ?? call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : undefined, isDynamic, parserWarning: evidence.parserWarning });
164
- const entityKind = callType;
165
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'HANDLER_ACCESSES_REMOTE_ENTITY', 'terminal', 'call', String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence, remoteEntityAccess: entityKind }), 0, generation);
166
- return { status: 'terminal', callType };
167
- }
168
- if (callType === 'remote_query' && (isEntityQueryIntent || !op) && !resolution.target) {
169
- const target = buildRemoteQueryTarget({ queryEntity: call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : undefined, isDynamic, parserWarning: evidence.parserWarning });
170
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'HANDLER_RUNS_REMOTE_QUERY', 'terminal', 'call', String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence }), 0, generation);
171
- return { status: 'terminal', callType };
172
- }
173
- if (callType === 'local_service_call' && call.unresolved_reason === 'transport_client_method' && !resolution.target && resolution.candidates.length === 0) {
174
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'HANDLER_CALLS_TRANSPORT_METHOD', 'terminal', 'call', String(call.id), 'transport_method', String(op || 'transport_client_method'), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: 'transport_client_method' }), 0, generation);
175
- return { status: 'terminal', callType };
176
- }
177
- if (resolution.target) {
178
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, callType === 'local_service_call' ? 'LOCAL_CALL_RESOLVES_TO_OPERATION' : 'REMOTE_CALL_RESOLVES_TO_OPERATION', 'resolved', 'call', String(call.id), 'operation', String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), 0, generation);
179
- return { status: 'resolved', callType };
180
- }
181
- const edgeType = callType === 'local_db_query' ? 'HANDLER_RUNS_DB_QUERY' : callType === 'external_http' ? 'HANDLER_CALLS_EXTERNAL_HTTP' : callType === 'async_emit' ? 'HANDLER_EMITS_EVENT' : callType === 'async_subscribe' ? 'EVENT_CONSUMED_BY_HANDLER' : resolution.status === 'dynamic' ? 'DYNAMIC_EDGE_CANDIDATE' : 'UNRESOLVED_EDGE';
182
- const status = edgeType === 'DYNAMIC_EDGE_CANDIDATE' ? 'dynamic' : resolution.status === 'ambiguous' ? 'ambiguous' : edgeType === 'UNRESOLVED_EDGE' ? 'unresolved' : 'terminal';
183
- const unresolvedReason = status === 'terminal' ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
184
- const externalTarget = callType === 'external_http' ? externalHttpTarget(call) : undefined;
185
- const targetKind = callType === 'local_db_query' ? 'db_entity' : callType.startsWith('async_') ? 'event' : callType === 'external_http' ? (externalTarget?.toKind ?? 'external_endpoint') : 'operation_candidate';
186
- const targetId = callType === 'local_db_query' ? String(call.query_entity ?? 'unknown') : callType === 'remote_action' ? (op ? `Remote action: ${op}` : (call.unresolved_reason === 'dynamic_operation_path_identifier' ? 'Remote action: dynamic path' : 'Remote action: unknown path')) : callType === 'external_http' ? String(externalTarget?.toId ?? 'unknown') : String(call.event_name_expr ?? op ?? 'unknown');
187
- const graphLevelDynamic = edgeType === 'DYNAMIC_EDGE_CANDIDATE' && resolution.status === 'dynamic';
188
- const finalEvidence = externalTarget ? { ...evidence, externalTarget } : evidence;
189
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, edgeType, status, 'call', String(call.id), targetKind, targetId, Number(call.confidence ?? 0.2), JSON.stringify(finalEvidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
190
- return { status, callType };
191
- }
192
- function operationCandidateCount(db: Db, workspaceId: number, operationPath: string | undefined, operationName: string | undefined): number {
193
- if (!operationPath && !operationName) return 0;
194
- const normalizedName = operationName ?? operationPath?.replace(/^\//, '').split('.').at(-1);
195
- const row = db.prepare(`SELECT COUNT(*) count FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_path=? OR o.operation_path=? OR o.operation_name=?)`).get(workspaceId, operationPath, normalizedName ? `/${normalizedName}` : operationPath, normalizedName) as { count?: number } | undefined;
196
- return Number(row?.count ?? 0);
197
- }
198
-
199
- function operationPrecedence(
200
- callType: string,
201
- intent: ReturnType<typeof classifyODataPathIntent>,
202
- indexedOperationCandidateCount: number,
203
- resolvedOperation: boolean,
204
- ): Record<string, unknown> {
205
- if (resolvedOperation) {
206
- return {
207
- decision: 'operation',
208
- reason: 'indexed_operation_with_strong_service_context',
209
- indexedOperationCandidateCount,
210
- };
211
- }
212
- if (callType === 'remote_action' && intent.kind === 'operation_invocation') {
213
- return {
214
- decision: 'operation_candidate',
215
- rejectionReason: indexedOperationCandidateCount > 0
216
- ? 'indexed_candidates_lack_unique_strong_service_context'
217
- : 'no_indexed_operation_candidate',
218
- indexedOperationCandidateCount,
219
- };
220
- }
221
- if (intent.kind.startsWith('entity_')) {
222
- return {
223
- decision: 'entity',
224
- rejectionReason: indexedOperationCandidateCount > 0
225
- ? 'entity_shape_has_precedence_without_resolved_operation_context'
226
- : 'entity_shape_has_no_indexed_operation_evidence',
227
- indexedOperationCandidateCount,
228
- };
229
- }
230
- return {
231
- decision: 'unresolved',
232
- rejectionReason: 'path_has_no_safe_entity_or_operation_precedence',
233
- indexedOperationCandidateCount,
234
- };
235
- }
236
-
237
- function unresolvedOperationReason(resolution: { candidates: unknown[]; status: string; reasons: string[] }): string {
238
- if (resolution.status === 'dynamic') return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ['missing runtime variables']).join(', ')}`;
239
- if (resolution.candidates.length === 0) return 'No indexed target operation matched';
240
- if (resolution.reasons.includes('operation_path_only_has_no_strong_target_signal')) return 'Operation candidates found but no strong service signal is available';
241
- if (resolution.reasons.includes('candidate_score_below_resolution_threshold')) return 'Operation candidates found but resolution score is below threshold';
242
- if (resolution.status === 'ambiguous') return 'Ambiguous operation candidates require a strong service signal';
243
- return 'Operation candidates found but resolution could not select a target';
244
- }
@@ -1,4 +1,4 @@
1
- import { extractPlaceholderKeys, scanPlaceholders } from '../utils/001-placeholders.js';
1
+ import { extractPlaceholderKeys, scanPlaceholders } from '../utils/placeholders.js';
2
2
 
3
3
  export interface RuntimeSubstitution {
4
4
  original?: string;
@@ -1,4 +1,8 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import {
3
+ linkEventTemplate,
4
+ type LinkedEventTemplate,
5
+ } from './event-template-link.js';
2
6
 
3
7
  export interface SubscriptionHandlerLinkSummary {
4
8
  edgeCount: number;
@@ -20,6 +24,7 @@ interface SubscriptionRow {
20
24
  startOffset?: number | null;
21
25
  endOffset?: number | null;
22
26
  confidence: number;
27
+ unresolvedReason?: string | null;
23
28
  }
24
29
 
25
30
  interface HandlerCallRow {
@@ -58,7 +63,7 @@ function subscriptionRows(db: Db, workspaceId: number): SubscriptionRow[] {
58
63
  c.source_symbol_id sourceSymbolId,c.event_name_expr eventName,
59
64
  c.source_file sourceFile,c.source_line sourceLine,
60
65
  c.call_site_start_offset startOffset,c.call_site_end_offset endOffset,
61
- c.confidence
66
+ c.confidence,c.unresolved_reason unresolvedReason
62
67
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
63
68
  WHERE r.workspace_id=? AND c.call_type='async_subscribe'
64
69
  AND json_extract(c.evidence_json,'$.handlerReferenceStatus')
@@ -202,11 +207,16 @@ function missingAssociation(
202
207
  function evidenceFor(
203
208
  subscription: SubscriptionRow,
204
209
  association: HandlerAssociation,
210
+ event: LinkedEventTemplate,
205
211
  ): Record<string, unknown> {
206
212
  const call: Partial<HandlerCallRow> = association.call ?? {};
207
213
  const symbolCallReason = boundedSymbolCallReason(call.unresolvedReason);
208
214
  return {
209
215
  eventName: subscription.eventName,
216
+ ...(event.substitution.placeholders.length > 0 ? {
217
+ effectiveEventName: event.targetId,
218
+ eventTemplateResolution: event.substitution,
219
+ } : {}),
210
220
  associationBasis: 'exact_subscription_call_span',
211
221
  dispatchScope: 'workspace_event_name_only',
212
222
  subscribeCallId: subscription.id,
@@ -253,22 +263,27 @@ function insertAssociationEdge(
253
263
  generation: number,
254
264
  subscription: SubscriptionRow,
255
265
  association: HandlerAssociation,
266
+ event: LinkedEventTemplate,
256
267
  ): void {
268
+ const status = event.isDynamic ? 'unresolved' : association.status;
269
+ const reason = event.isDynamic
270
+ ? 'event_template_variables_missing'
271
+ : association.reasonCode ?? association.call?.unresolvedReason ?? null;
257
272
  db.prepare(`INSERT INTO graph_edges(
258
273
  workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
259
274
  confidence,evidence_json,is_dynamic,unresolved_reason,generation
260
275
  ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
261
276
  workspaceId,
262
277
  'EVENT_SUBSCRIPTION_HANDLED_BY',
263
- association.status,
264
- 'event',
265
- subscription.eventName,
278
+ status,
279
+ event.targetKind,
280
+ event.targetId,
266
281
  association.toKind,
267
282
  association.toId,
268
283
  association.call?.confidence ?? subscription.confidence,
269
- JSON.stringify(evidenceFor(subscription, association)),
270
- 0,
271
- association.reasonCode ?? association.call?.unresolvedReason ?? null,
284
+ JSON.stringify(evidenceFor(subscription, association, event)),
285
+ event.isDynamic ? 1 : 0,
286
+ reason,
272
287
  generation,
273
288
  );
274
289
  }
@@ -277,6 +292,7 @@ export function linkEventSubscriptionHandlers(
277
292
  db: Db,
278
293
  workspaceId: number,
279
294
  generation: number,
295
+ variables: Record<string, string> = {},
280
296
  ): SubscriptionHandlerLinkSummary {
281
297
  const summary: SubscriptionHandlerLinkSummary = {
282
298
  edgeCount: 0,
@@ -286,16 +302,21 @@ export function linkEventSubscriptionHandlers(
286
302
  missingAssociationCount: 0,
287
303
  };
288
304
  for (const subscription of subscriptionRows(db, workspaceId)) {
305
+ const event = linkEventTemplate(
306
+ subscription.eventName, variables,
307
+ subscription.unresolvedReason ?? undefined,
308
+ );
289
309
  const association = associationFor(
290
310
  subscription, roleSiteRows(db, subscription),
291
311
  );
292
312
  insertAssociationEdge(
293
- db, workspaceId, generation, subscription, association,
313
+ db, workspaceId, generation, subscription, association, event,
294
314
  );
315
+ const status = event.isDynamic ? 'unresolved' : association.status;
295
316
  summary.edgeCount += 1;
296
- summary.resolvedCount += association.status === 'resolved' ? 1 : 0;
297
- summary.ambiguousCount += association.status === 'ambiguous' ? 1 : 0;
298
- summary.unresolvedCount += association.status === 'unresolved' ? 1 : 0;
317
+ summary.resolvedCount += status === 'resolved' ? 1 : 0;
318
+ summary.ambiguousCount += status === 'ambiguous' ? 1 : 0;
319
+ summary.unresolvedCount += status === 'unresolved' ? 1 : 0;
299
320
  summary.missingAssociationCount += association.missing ? 1 : 0;
300
321
  }
301
322
  return summary;
@@ -0,0 +1,72 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ substituteVariables,
4
+ type RuntimeSubstitution,
5
+ } from './dynamic-edge-resolver.js';
6
+
7
+ export interface LinkedEventTemplate {
8
+ targetId: string;
9
+ targetKind: 'event' | 'event_candidate';
10
+ status: 'terminal' | 'dynamic';
11
+ isDynamic: boolean;
12
+ unresolvedReason?: string;
13
+ substitution: RuntimeSubstitution;
14
+ }
15
+
16
+ export function linkEventTemplate(
17
+ template: string,
18
+ variables: Record<string, string>,
19
+ parserReason?: string,
20
+ ): LinkedEventTemplate {
21
+ const substitution = substituteVariables(template, variables);
22
+ const missing = substitution.missing.length > 0;
23
+ const unsupportedDynamic = substitution.placeholders.length === 0
24
+ && parserReason !== undefined;
25
+ const dynamic = missing || unsupportedDynamic;
26
+ return {
27
+ targetId: dynamic
28
+ ? `Event: ${substitution.effective ?? template}`
29
+ : substitution.effective ?? template,
30
+ targetKind: dynamic ? 'event_candidate' : 'event',
31
+ status: dynamic ? 'dynamic' : 'terminal',
32
+ isDynamic: dynamic,
33
+ unresolvedReason: missing
34
+ ? `Dynamic target requires runtime variable overrides: ${
35
+ substitution.missing.join(', ')}`
36
+ : unsupportedDynamic ? parserReason : undefined,
37
+ substitution,
38
+ };
39
+ }
40
+
41
+ export function insertEventCallEdge(
42
+ db: Db,
43
+ workspaceId: number,
44
+ generation: number,
45
+ call: Record<string, unknown>,
46
+ variables: Record<string, string>,
47
+ evidence: Record<string, unknown>,
48
+ ): { status: string; callType: string } {
49
+ const callType = String(call.call_type);
50
+ const event = linkEventTemplate(
51
+ String(call.event_name_expr ?? ''), variables,
52
+ typeof call.unresolved_reason === 'string'
53
+ ? call.unresolved_reason : undefined,
54
+ );
55
+ const edgeType = event.isDynamic
56
+ ? 'DYNAMIC_EDGE_CANDIDATE'
57
+ : callType === 'async_emit'
58
+ ? 'HANDLER_EMITS_EVENT' : 'EVENT_CONSUMED_BY_HANDLER';
59
+ const eventEvidence = event.substitution.placeholders.length > 0
60
+ ? { ...evidence, eventTemplateResolution: event.substitution }
61
+ : evidence;
62
+ db.prepare(`INSERT INTO graph_edges(
63
+ workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
64
+ confidence,evidence_json,is_dynamic,unresolved_reason,generation
65
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
66
+ workspaceId, edgeType, event.status, 'call', String(call.id),
67
+ event.targetKind, event.targetId, Number(call.confidence ?? 0.2),
68
+ JSON.stringify(eventEvidence), event.isDynamic ? 1 : 0,
69
+ event.unresolvedReason ?? null, generation,
70
+ );
71
+ return { status: event.status, callType };
72
+ }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { projectBounded } from '../utils/000-bounded-projection.js';
2
+ import { projectBounded } from '../utils/bounded-projection.js';
3
3
 
4
4
  export type ExternalTargetKind = 'destination' | 'static_url' | 'url_expression' | 'unknown';
5
5
  export interface ExternalHttpTarget { kind: ExternalTargetKind; toKind: 'external_destination' | 'external_endpoint'; toId: string; label: string; method?: string; dynamic: boolean; expression?: string; candidateLiteralCount?: number; shownCandidateLiteralCount?: number; omittedCandidateLiteralCount?: number; }
@@ -1,5 +1,5 @@
1
1
  import type { Db } from '../db/connection.js';
2
- import { projectBounded } from '../utils/000-bounded-projection.js';
2
+ import { projectBounded } from '../utils/bounded-projection.js';
3
3
 
4
4
  interface RepoDependencyRow {
5
5
  id: number;
@@ -6,7 +6,7 @@ import {
6
6
  boundedImplementationTargetIds,
7
7
  displayImplementationCandidates,
8
8
  selectedHandlerProvenance,
9
- } from './001-implementation-evidence-projection.js';
9
+ } from './implementation-evidence-projection.js';
10
10
 
11
11
  interface ImplementationCandidate extends Record<string, unknown> {
12
12
  methodId: number;
@@ -2,7 +2,7 @@ import {
2
2
  projectBounded,
3
3
  projectBoundedInOrder,
4
4
  type BoundedProjection,
5
- } from '../utils/000-bounded-projection.js';
5
+ } from '../utils/bounded-projection.js';
6
6
 
7
7
  export interface SelectedHandlerSource {
8
8
  methodId: number;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeODataPathStructure,
3
3
  type ODataPathStructure,
4
- } from './005-odata-path-structure.js';
4
+ } from './odata-path-structure.js';
5
5
 
6
6
  export interface NormalizedODataOperationPath {
7
7
  rawOperationPath: string;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  scanPlaceholderStructure,
3
3
  type PlaceholderSpan,
4
- } from '../utils/001-placeholders.js';
4
+ } from '../utils/placeholders.js';
5
5
 
6
6
  export type ODataPathStructureReason =
7
7
  | 'path_parenthesis_is_unbalanced'
@@ -3,14 +3,14 @@ import type {
3
3
  PackagePublicScope,
4
4
  PackagePublicSurfaceFact,
5
5
  PublicSurfaceTarget,
6
- } from '../parsers/003-package-public-surface.js';
6
+ } from '../parsers/package-public-surface.js';
7
7
  import type {
8
8
  SymbolImportReference,
9
- } from '../parsers/002-symbol-import-bindings.js';
9
+ } from '../parsers/symbol-import-bindings.js';
10
10
  import {
11
11
  parsePackageImportReference,
12
12
  parsePackagePublicSurfaceFact,
13
- } from '../parsers/012-package-fact-contract.js';
13
+ } from '../parsers/package-fact-contract.js';
14
14
 
15
15
  export interface PackageSymbolLinkSummary {
16
16
  resolved: number;
@@ -1,6 +1,6 @@
1
1
  import type { Db } from '../db/connection.js';
2
- import { extractPlaceholderKeys } from '../utils/001-placeholders.js';
3
- import { canonicalImplementationEvidence } from './000-implementation-candidates.js';
2
+ import { extractPlaceholderKeys } from '../utils/placeholders.js';
3
+ import { canonicalImplementationEvidence } from './implementation-candidates.js';
4
4
  export interface OperationTarget {
5
5
  operationId: number;
6
6
  repoName: string;
@@ -1,4 +1,4 @@
1
- import type { CompactGraphV1 } from '../trace/014-compact-contract.js';
1
+ import type { CompactGraphV1 } from '../trace/compact-contract.js';
2
2
  import { redactValue } from '../utils/redaction.js';
3
3
 
4
4
  export function renderCompactJson(value: CompactGraphV1): string {
@@ -9,7 +9,7 @@ import type {
9
9
  import {
10
10
  executableSymbolCandidates,
11
11
  selectCallOwner,
12
- } from './004-fact-identity.js';
12
+ } from './fact-identity.js';
13
13
  import {
14
14
  bindingSite,
15
15
  createBindingLexicalIndex,
@@ -17,8 +17,8 @@ import {
17
17
  type BindingLexicalIndex,
18
18
  type BindingLexicalSite,
19
19
  type VisibleBinding,
20
- } from './011-binding-lexical-scope.js';
21
- import { selectVisibleBinding } from './021-binding-visibility.js';
20
+ } from './binding-lexical-scope.js';
21
+ import { selectVisibleBinding } from './binding-visibility.js';
22
22
 
23
23
  const scopeChainCap = 16;
24
24
 
@@ -230,6 +230,9 @@ function resolvedReference(
230
230
  chain: LexicalScopeFact[],
231
231
  scopeIndex: number,
232
232
  ): ServiceBindingReference {
233
+ const helperReturn = selected.fact.helperChain?.some(
234
+ (step) => step.bindingOrigin === 'single_hop_helper_return',
235
+ ) ?? false;
233
236
  return {
234
237
  status: 'resolved_exact',
235
238
  variableName: call.serviceVariableName,
@@ -240,7 +243,9 @@ function resolvedReference(
240
243
  ? 'deterministic_reaching_assignment'
241
244
  : selected.site.aliasSource
242
245
  ? 'lexical_alias_declaration'
243
- : 'lexical_declaration',
246
+ : helperReturn
247
+ ? 'single_hop_helper_return'
248
+ : 'lexical_declaration',
244
249
  lexicalScopeChain: chain,
245
250
  bindingScopeIndex: scopeIndex,
246
251
  scopeChainTotal: chain.length,
@@ -3,7 +3,7 @@ import type { LexicalScopeFact } from '../types.js';
3
3
  import {
4
4
  bindingAssignmentEntries,
5
5
  type BindingAssignmentEntry,
6
- } from './019-binding-assignment-targets.js';
6
+ } from './binding-assignment-targets.js';
7
7
 
8
8
  export type BindingDeclarationKind =
9
9
  | 'const'
@@ -10,7 +10,7 @@ import {
10
10
  type BindingLexicalSite,
11
11
  type BindingSiteCandidate,
12
12
  type VisibleBinding,
13
- } from './011-binding-lexical-scope.js';
13
+ } from './binding-lexical-scope.js';
14
14
 
15
15
  function reachingSites(
16
16
  index: BindingLexicalIndex,
@@ -72,6 +72,20 @@ function candidatesAtSite<T>(
72
72
  && candidate.bindingSiteEndOffset === site.endOffset);
73
73
  }
74
74
 
75
+ function helperCallDeclaration(site: BindingLexicalSite): boolean {
76
+ if (!ts.isVariableDeclaration(site.node) || !site.node.initializer)
77
+ return false;
78
+ let expression = site.node.initializer;
79
+ while (ts.isAwaitExpression(expression)
80
+ || ts.isParenthesizedExpression(expression)
81
+ || ts.isAsExpression(expression)
82
+ || ts.isSatisfiesExpression(expression)
83
+ || ts.isTypeAssertionExpression(expression))
84
+ expression = expression.expression;
85
+ return ts.isCallExpression(expression)
86
+ && ts.isIdentifier(expression.expression);
87
+ }
88
+
75
89
  function compatibleCaseClause(
76
90
  site: BindingLexicalSite,
77
91
  useNode: ts.Node,
@@ -108,7 +122,9 @@ function matchedBinding<T>(
108
122
  status: 'unresolved',
109
123
  reason: site.flow === 'assignment'
110
124
  ? 'unsupported_reaching_assignment'
111
- : 'binding_not_found',
125
+ : helperCallDeclaration(site)
126
+ ? 'binding_flow_unsupported'
127
+ : 'binding_not_found',
112
128
  };
113
129
  if (matches.length > 1) return { status: 'ambiguous' };
114
130
  return matches[0] ? { status: 'resolved', candidate: matches[0] } : undefined;
@@ -43,6 +43,17 @@ export function queryBuilderRoot(
43
43
  : undefined;
44
44
  }
45
45
 
46
+ export function chainIncludesForUpdate(node: ts.Expression): boolean {
47
+ let current = unwrapQueryExpression(node);
48
+ while (ts.isCallExpression(current)
49
+ && ts.isPropertyAccessExpression(current.expression)) {
50
+ if (isCapQueryBuilderRootName(expressionName(current.expression))) break;
51
+ if (current.expression.name.text === 'forUpdate') return true;
52
+ current = unwrapQueryExpression(current.expression.expression);
53
+ }
54
+ return false;
55
+ }
56
+
46
57
  export function directQueryBuilderStatement(
47
58
  node: ts.CallExpression,
48
59
  ): DirectQueryBuilderStatement | undefined {
@@ -9,12 +9,12 @@ import {
9
9
  symbolImportReference,
10
10
  type SymbolImportBinding,
11
11
  type SymbolImportReference,
12
- } from './002-symbol-import-bindings.js';
12
+ } from './symbol-import-bindings.js';
13
13
  import type { ClassifiedOutboundCall } from './outbound-call-parser.js';
14
14
  import {
15
15
  localSymbolTarget,
16
16
  type LocalSymbolTargetIdentity,
17
- } from './016-local-symbol-reference.js';
17
+ } from './local-symbol-reference.js';
18
18
 
19
19
  interface HandlerTarget {
20
20
  calleeExpression: string;
@@ -3,8 +3,8 @@ import type { ExecutableSymbolFact } from '../types.js';
3
3
  import {
4
4
  lexicalIdentifierDeclaration,
5
5
  lexicalIdentifierDeclarations,
6
- } from './002-symbol-import-bindings.js';
7
- import { stableLocalValueReference } from './020-stable-local-value.js';
6
+ } from './symbol-import-bindings.js';
7
+ import { stableLocalValueReference } from './stable-local-value.js';
8
8
 
9
9
  export interface LocalSymbolTargetIdentity {
10
10
  sourceFile: string;
@@ -3,7 +3,7 @@ import {
3
3
  classifyODataPathIntent,
4
4
  normalizeODataOperationInvocationPath,
5
5
  } from '../linker/odata-path-normalizer.js';
6
- import { analyzeODataPathStructure } from '../linker/005-odata-path-structure.js';
6
+ import { analyzeODataPathStructure } from '../linker/odata-path-structure.js';
7
7
 
8
8
  export type OperationPathStatus = 'static' | 'ambiguous' | 'dynamic' | 'unknown';
9
9