@saptools/service-flow 0.1.52 → 0.1.54

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 (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +11 -12
  3. package/TECHNICAL-NOTE.md +18 -3
  4. package/dist/{chunk-PTLDSHRC.js → chunk-ERIZHM5C.js} +2646 -1067
  5. package/dist/chunk-ERIZHM5C.js.map +1 -0
  6. package/dist/cli.js +162 -13
  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 +136 -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 +674 -0
  14. package/src/linker/001-implementation-evidence-projection.ts +191 -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 +15 -4
  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/008-contextual-runtime-state.ts +294 -0
  31. package/src/trace/009-selected-handler-provenance.ts +186 -0
  32. package/src/trace/dynamic-targets.ts +205 -159
  33. package/src/trace/evidence.ts +132 -34
  34. package/src/trace/implementation-hints.ts +148 -8
  35. package/src/trace/selectors.ts +74 -9
  36. package/src/trace/trace-engine.ts +97 -125
  37. package/src/utils/000-bounded-projection.ts +166 -0
  38. package/dist/chunk-PTLDSHRC.js.map +0 -1
@@ -0,0 +1,294 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
3
+ import { resolveOperation, type OperationResolution } from '../linker/service-resolver.js';
4
+ import { boundedContextCandidates } from './006-contextual-projection.js';
5
+
6
+ export interface ContextBinding {
7
+ bindingId?: number;
8
+ alias?: string;
9
+ aliasExpr?: string;
10
+ destinationExpr?: string;
11
+ servicePathExpr?: string;
12
+ requireServicePath?: string;
13
+ requireDestination?: string;
14
+ effectiveServicePath?: string;
15
+ effectiveDestination?: string;
16
+ sourceFile?: string;
17
+ sourceLine?: number;
18
+ source: string;
19
+ callerArgument?: string;
20
+ callerProperty?: string;
21
+ calleeParameter?: string;
22
+ calleeObjectProperty?: string;
23
+ calleeLocalDestructuredIdentifier?: string;
24
+ parameterPropertyAliasKind?: unknown;
25
+ parameterPropertyAliasLine?: unknown;
26
+ calleeReceiver: string;
27
+ callerSite?: { sourceFile?: string; sourceLine?: number };
28
+ calleeSite?: { sourceFile?: string; sourceLine?: number };
29
+ resolutionStatus?: 'selected' | 'ambiguous';
30
+ bindingCandidates?: Array<Record<string, unknown>>;
31
+ }
32
+
33
+ interface ContextualCall {
34
+ id: number;
35
+ call_type: string;
36
+ operation_path_expr?: unknown;
37
+ }
38
+
39
+ interface PersistedGraphRow {
40
+ status?: string;
41
+ }
42
+
43
+ export interface ContextualGraphRow {
44
+ id: number;
45
+ edge_type: string;
46
+ from_id: string;
47
+ to_kind: string;
48
+ to_id: string;
49
+ confidence: number;
50
+ evidence_json: string;
51
+ status: 'resolved';
52
+ }
53
+
54
+ export type ContextualResolutionCategory =
55
+ | 'none'
56
+ | 'dynamic_missing'
57
+ | 'ambiguous_binding'
58
+ | 'ambiguous_operation'
59
+ | 'no_matching_operation'
60
+ | 'other_blocker';
61
+
62
+ export interface ContextualRuntimeState {
63
+ category: ContextualResolutionCategory;
64
+ message?: string;
65
+ missingVariables?: string[];
66
+ resolutionStatus?: string;
67
+ phase?: 'before_runtime_substitution';
68
+ }
69
+
70
+ export interface ContextualRuntimeResolution {
71
+ row?: ContextualGraphRow;
72
+ evidence?: Record<string, unknown>;
73
+ state: ContextualRuntimeState;
74
+ }
75
+
76
+ export function dynamicMissingReason(keys: readonly string[]): string {
77
+ const missing = normalizedKeys(keys);
78
+ return missing.length > 0
79
+ ? `Dynamic target is missing runtime variables: ${missing.join(', ')}`
80
+ : 'Dynamic target still requires runtime variables';
81
+ }
82
+
83
+ export function isStructuralContextualBlocker(
84
+ state: ContextualRuntimeState | undefined,
85
+ ): boolean {
86
+ return state?.category === 'ambiguous_binding'
87
+ || state?.category === 'ambiguous_operation'
88
+ || state?.category === 'no_matching_operation'
89
+ || state?.category === 'other_blocker';
90
+ }
91
+
92
+ export function contextualRuntimeResolution(
93
+ db: Db,
94
+ call: ContextualCall,
95
+ binding: ContextBinding | undefined,
96
+ workspaceId: number | undefined,
97
+ persistedRows: PersistedGraphRow[] = [],
98
+ ): ContextualRuntimeResolution {
99
+ if (!binding || call.call_type !== 'remote_action'
100
+ || call.operation_path_expr === undefined || call.operation_path_expr === null)
101
+ return { state: { category: 'none' } };
102
+ if (binding.resolutionStatus === 'ambiguous')
103
+ return ambiguousBindingResolution(binding);
104
+ return selectedBindingResolution(db, call, binding, workspaceId, persistedRows);
105
+ }
106
+
107
+ function ambiguousBindingResolution(
108
+ binding: ContextBinding,
109
+ ): ContextualRuntimeResolution {
110
+ const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
111
+ const state: ContextualRuntimeState = {
112
+ category: 'ambiguous_binding',
113
+ message: 'Ambiguous contextual service binding candidates',
114
+ resolutionStatus: 'ambiguous',
115
+ };
116
+ return {
117
+ evidence: {
118
+ contextualServiceBindingAttempted: true,
119
+ contextualBinding: {
120
+ source: binding.source,
121
+ status: 'tied',
122
+ candidates: candidates.candidates,
123
+ candidateCount: candidates.candidateCount,
124
+ shownCandidateCount: candidates.shownCandidateCount,
125
+ omittedCandidateCount: candidates.omittedCandidateCount,
126
+ },
127
+ contextualResolutionStatus: 'ambiguous',
128
+ contextualCandidateCount: candidates.candidateCount,
129
+ contextualPreSubstitutionState: historicalState(state),
130
+ },
131
+ state,
132
+ };
133
+ }
134
+
135
+ function selectedBindingResolution(
136
+ db: Db,
137
+ call: ContextualCall,
138
+ binding: ContextBinding,
139
+ workspaceId: number | undefined,
140
+ persistedRows: PersistedGraphRow[],
141
+ ): ContextualRuntimeResolution {
142
+ const normalized = normalizeODataOperationInvocationPath(
143
+ String(call.operation_path_expr),
144
+ );
145
+ const operationPath = normalized?.normalizedOperationPath
146
+ ?? withLeadingSlash(String(call.operation_path_expr));
147
+ const servicePath = binding.effectiveServicePath
148
+ ?? binding.servicePathExpr ?? binding.requireServicePath;
149
+ const destination = binding.effectiveDestination
150
+ ?? binding.destinationExpr ?? binding.requireDestination;
151
+ const resolution = resolveOperation(db, {
152
+ servicePath,
153
+ operationPath,
154
+ alias: binding.aliasExpr ?? binding.alias,
155
+ destination,
156
+ hasExplicitOverride: true,
157
+ isDynamic: false,
158
+ }, workspaceId);
159
+ const state = stateForResolution(resolution);
160
+ const evidence = contextualEvidence(
161
+ binding, normalized, operationPath, servicePath, destination, resolution, state,
162
+ );
163
+ if (!resolution.target) return { evidence, state };
164
+ const resolvedEvidence = {
165
+ ...evidence,
166
+ contextualServiceBindingSelected: true,
167
+ targetRepo: resolution.target.repoName,
168
+ targetServicePath: resolution.target.servicePath,
169
+ targetOperationPath: resolution.target.operationPath,
170
+ targetOperation: resolution.target.operationName,
171
+ };
172
+ if (persistedRows.some((row) => row.status === 'resolved'))
173
+ return { evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, state };
174
+ return {
175
+ row: {
176
+ id: -call.id,
177
+ edge_type: 'REMOTE_CALL_RESOLVES_TO_OPERATION',
178
+ from_id: String(call.id),
179
+ to_kind: 'operation',
180
+ to_id: String(resolution.target.operationId),
181
+ confidence: resolution.target.score,
182
+ evidence_json: JSON.stringify(resolvedEvidence),
183
+ status: 'resolved',
184
+ },
185
+ evidence: resolvedEvidence,
186
+ state,
187
+ };
188
+ }
189
+
190
+ function contextualEvidence(
191
+ binding: ContextBinding,
192
+ normalized: ReturnType<typeof normalizeODataOperationInvocationPath>,
193
+ operationPath: string,
194
+ servicePath: string | undefined,
195
+ destination: string | undefined,
196
+ resolution: OperationResolution,
197
+ state: ContextualRuntimeState,
198
+ ): Record<string, unknown> {
199
+ const candidates = boundedContextCandidates(resolution.candidates);
200
+ return {
201
+ contextualServiceBindingAttempted: true,
202
+ contextualBinding: bindingEvidence(binding),
203
+ operationPath,
204
+ rawOperationPath: normalized?.rawOperationPath,
205
+ normalizedOperationPath: normalized?.wasInvocation
206
+ ? normalized.normalizedOperationPath : undefined,
207
+ invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length
208
+ ? normalized.invocationArgumentPlaceholderKeys : undefined,
209
+ servicePath,
210
+ serviceAlias: binding.alias,
211
+ serviceAliasExpr: binding.aliasExpr,
212
+ destination,
213
+ requireServicePath: binding.requireServicePath,
214
+ requireDestination: binding.requireDestination,
215
+ effectiveServicePath: binding.effectiveServicePath,
216
+ effectiveDestination: binding.effectiveDestination,
217
+ contextualResolutionStatus: resolution.status,
218
+ contextualCandidateCount: candidates.candidateCount,
219
+ shownContextualCandidateCount: candidates.shownCandidateCount,
220
+ omittedContextualCandidateCount: candidates.omittedCandidateCount,
221
+ candidates: candidates.candidates,
222
+ contextualResolutionReasons: resolution.reasons,
223
+ resolutionReasons: resolution.reasons,
224
+ contextualPreSubstitutionState: historicalState(state),
225
+ };
226
+ }
227
+
228
+ function bindingEvidence(binding: ContextBinding): Record<string, unknown> {
229
+ return {
230
+ source: binding.source,
231
+ callerArgument: binding.callerArgument,
232
+ callerProperty: binding.callerProperty,
233
+ calleeParameter: binding.calleeParameter,
234
+ calleeReceiver: binding.calleeReceiver,
235
+ callerSite: binding.callerSite,
236
+ calleeSite: binding.calleeSite,
237
+ bindingSourceFile: binding.sourceFile,
238
+ bindingSourceLine: binding.sourceLine,
239
+ alias: binding.alias,
240
+ aliasExpr: binding.aliasExpr,
241
+ requireServicePath: binding.requireServicePath,
242
+ requireDestination: binding.requireDestination,
243
+ effectiveServicePath: binding.effectiveServicePath,
244
+ effectiveDestination: binding.effectiveDestination,
245
+ };
246
+ }
247
+
248
+ function stateForResolution(
249
+ resolution: OperationResolution,
250
+ ): ContextualRuntimeState {
251
+ if (resolution.status === 'resolved') return { category: 'none' };
252
+ if (resolution.status === 'dynamic') {
253
+ const missingVariables = missingVariableKeys(resolution.reasons);
254
+ return {
255
+ category: 'dynamic_missing',
256
+ message: dynamicMissingReason(missingVariables),
257
+ missingVariables,
258
+ resolutionStatus: resolution.status,
259
+ };
260
+ }
261
+ if (resolution.status === 'ambiguous') return {
262
+ category: 'ambiguous_operation',
263
+ message: 'Ambiguous contextual operation candidates',
264
+ resolutionStatus: resolution.status,
265
+ };
266
+ return {
267
+ category: resolution.status === 'unresolved'
268
+ ? 'no_matching_operation' : 'other_blocker',
269
+ message: resolution.status === 'unresolved'
270
+ ? 'No contextual operation candidate matched'
271
+ : 'Contextual operation resolution is blocked',
272
+ resolutionStatus: resolution.status,
273
+ };
274
+ }
275
+
276
+ function historicalState(
277
+ state: ContextualRuntimeState,
278
+ ): ContextualRuntimeState {
279
+ return { ...state, phase: 'before_runtime_substitution' };
280
+ }
281
+
282
+ function missingVariableKeys(reasons: string[]): string[] {
283
+ return normalizedKeys(reasons.flatMap((reason) =>
284
+ reason.startsWith('missing_variable:')
285
+ ? [reason.slice('missing_variable:'.length)] : []));
286
+ }
287
+
288
+ function normalizedKeys(keys: readonly string[]): string[] {
289
+ return [...new Set(keys.filter((key) => key.length > 0))].sort();
290
+ }
291
+
292
+ function withLeadingSlash(value: string): string {
293
+ return value.startsWith('/') ? value : `/${value}`;
294
+ }
@@ -0,0 +1,186 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ selectedHandlerProvenance,
4
+ type SelectedHandlerProvenance,
5
+ type SelectedHandlerSource,
6
+ } from '../linker/001-implementation-evidence-projection.js';
7
+
8
+ export interface HandlerMethodNode extends Record<string, unknown> {
9
+ id: string;
10
+ kind: 'handler_method';
11
+ label: string;
12
+ methodId: number;
13
+ methodName: string;
14
+ className: string;
15
+ sourceFile: string;
16
+ sourceLine: number;
17
+ repoId: number;
18
+ repoName: string;
19
+ packageName?: string;
20
+ }
21
+
22
+ export interface SelectedHandlerEvidence {
23
+ handler?: HandlerMethodNode;
24
+ evidence: Record<string, unknown>;
25
+ diagnostic?: Record<string, unknown>;
26
+ unresolvedReason?: string;
27
+ }
28
+
29
+ export function handlerMethodNode(
30
+ db: Db,
31
+ methodId: string,
32
+ ): HandlerMethodNode | undefined {
33
+ const row = db.prepare(`SELECT hm.id methodId,hm.method_name methodName,
34
+ hm.source_line sourceLine,hc.class_name className,hc.source_file sourceFile,
35
+ r.name repoName,r.id repoId,r.package_name packageName
36
+ FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id
37
+ JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(methodId) as
38
+ | Record<string, unknown>
39
+ | undefined;
40
+ return handlerNodeFromRow(row);
41
+ }
42
+
43
+ export function withSelectedHandlerProvenance(
44
+ evidence: Record<string, unknown>,
45
+ targetId: string,
46
+ handler: HandlerMethodNode | undefined,
47
+ ): SelectedHandlerEvidence {
48
+ if (!handler) return unavailableHandlerEvidence(evidence, targetId);
49
+ const selected = selectedHandlerProvenance(handlerSource(handler));
50
+ const stored = record(evidence.selectedHandler);
51
+ const mismatchedFields = storedSelectedHandlerMismatches(
52
+ stored, selected, targetId,
53
+ );
54
+ if (mismatchedFields.length === 0)
55
+ return { handler, evidence: { ...evidence, selectedHandler: selected } };
56
+ return {
57
+ handler,
58
+ evidence: {
59
+ ...evidence,
60
+ selectedHandler: selected,
61
+ selectedHandlerProvenanceAudit: {
62
+ status: 'mismatch',
63
+ graphTargetId: targetId,
64
+ mismatchedFields,
65
+ persistedSelectedHandler: stored,
66
+ },
67
+ },
68
+ diagnostic: {
69
+ severity: 'warning',
70
+ code: 'selected_handler_provenance_mismatch',
71
+ message: 'Persisted selected handler provenance did not match the resolved graph target',
72
+ graphTargetId: targetId,
73
+ mismatchedFields,
74
+ sourceFile: handler.sourceFile,
75
+ sourceLine: handler.sourceLine,
76
+ },
77
+ };
78
+ }
79
+
80
+ function unavailableHandlerEvidence(
81
+ evidence: Record<string, unknown>,
82
+ targetId: string,
83
+ ): SelectedHandlerEvidence {
84
+ return {
85
+ evidence: {
86
+ ...evidence,
87
+ selectedHandlerProvenanceAudit: {
88
+ status: 'unavailable',
89
+ graphTargetId: targetId,
90
+ reason: 'handler_target_not_indexed',
91
+ },
92
+ },
93
+ diagnostic: {
94
+ severity: 'warning',
95
+ code: 'selected_handler_target_not_found',
96
+ message: 'Resolved implementation target is not an indexed handler method',
97
+ graphTargetId: targetId,
98
+ },
99
+ unresolvedReason: 'Resolved implementation target is not an indexed handler method',
100
+ };
101
+ }
102
+
103
+ function handlerNodeFromRow(
104
+ row: Record<string, unknown> | undefined,
105
+ ): HandlerMethodNode | undefined {
106
+ const methodId = numberValue(row?.methodId);
107
+ const methodName = stringValue(row?.methodName);
108
+ const className = stringValue(row?.className);
109
+ const sourceFile = stringValue(row?.sourceFile);
110
+ const sourceLine = numberValue(row?.sourceLine);
111
+ const repoId = numberValue(row?.repoId);
112
+ const repoName = stringValue(row?.repoName);
113
+ if (methodId === undefined || !methodName || !className || !sourceFile
114
+ || sourceLine === undefined || repoId === undefined || !repoName)
115
+ return undefined;
116
+ return {
117
+ id: `handler_method:${methodId}`,
118
+ kind: 'handler_method',
119
+ label: `${repoName}:${className}.${methodName}`,
120
+ methodId,
121
+ methodName,
122
+ className,
123
+ sourceFile,
124
+ sourceLine,
125
+ repoId,
126
+ repoName,
127
+ packageName: stringValue(row?.packageName),
128
+ };
129
+ }
130
+
131
+ function handlerSource(node: HandlerMethodNode): SelectedHandlerSource {
132
+ return {
133
+ methodId: node.methodId,
134
+ methodName: node.methodName,
135
+ className: node.className,
136
+ repositoryId: node.repoId,
137
+ repositoryName: node.repoName,
138
+ repositoryPackageName: node.packageName,
139
+ sourceFile: node.sourceFile,
140
+ sourceLine: node.sourceLine,
141
+ };
142
+ }
143
+
144
+ function storedSelectedHandlerMismatches(
145
+ stored: Record<string, unknown>,
146
+ selected: SelectedHandlerProvenance,
147
+ targetId: string,
148
+ ): string[] {
149
+ if (Object.keys(stored).length === 0) return [];
150
+ const expectedRepository = record(stored.repository);
151
+ const selectedRepository = selected.repository ?? {};
152
+ return [
153
+ mismatch('graphTargetId', stored.graphTargetId, targetId),
154
+ mismatch('methodId', stored.methodId, selected.methodId),
155
+ mismatch('className', stored.className, selected.className),
156
+ mismatch('methodName', stored.methodName, selected.methodName),
157
+ mismatch('sourceFile', stored.sourceFile, selected.sourceFile),
158
+ mismatch('sourceLine', stored.sourceLine, selected.sourceLine),
159
+ mismatch('repository.id', expectedRepository.id, selectedRepository.id),
160
+ mismatch('repository.name', expectedRepository.name, selectedRepository.name),
161
+ mismatch('repository.packageName', expectedRepository.packageName,
162
+ selectedRepository.packageName),
163
+ ].flatMap((field) => field ? [field] : []);
164
+ }
165
+
166
+ function mismatch(
167
+ field: string,
168
+ stored: unknown,
169
+ actual: unknown,
170
+ ): string | undefined {
171
+ return stored === undefined || stored === actual ? undefined : field;
172
+ }
173
+
174
+ function record(value: unknown): Record<string, unknown> {
175
+ return value && typeof value === 'object' && !Array.isArray(value)
176
+ ? value as Record<string, unknown>
177
+ : {};
178
+ }
179
+
180
+ function stringValue(value: unknown): string | undefined {
181
+ return typeof value === 'string' ? value : undefined;
182
+ }
183
+
184
+ function numberValue(value: unknown): number | undefined {
185
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
186
+ }