@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,155 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import {
3
+ extractPlaceholders,
4
+ matchRuntimeTemplate,
5
+ } from '../linker/dynamic-edge-resolver.js';
6
+ import type { OperationTarget } from '../linker/service-resolver.js';
7
+
8
+ export function dynamicCandidateTargets(
9
+ db: Db,
10
+ effectiveOperationPath: string | undefined,
11
+ originalOperationPath: string | undefined,
12
+ embedded: unknown,
13
+ workspaceId: number | undefined,
14
+ requireCanonical: boolean,
15
+ ): OperationTarget[] {
16
+ const canonical = queryOperationTargets(
17
+ db, effectiveOperationPath, originalOperationPath, workspaceId,
18
+ );
19
+ if (canonical.length > 0 || requireCanonical) return canonical;
20
+ return targetsFromEvidence(embedded);
21
+ }
22
+
23
+ function targetsFromEvidence(value: unknown): OperationTarget[] {
24
+ if (!Array.isArray(value)) return [];
25
+ return value.flatMap((item): OperationTarget[] => {
26
+ const row = record(item);
27
+ const operationId = numberValue(row.operationId);
28
+ const repoName = stringValue(row.repoName);
29
+ const servicePath = stringValue(row.servicePath);
30
+ const operationPath = stringValue(row.operationPath);
31
+ const operationName = stringValue(row.operationName) ?? operationPath?.replace(/^\//, '');
32
+ if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName)
33
+ return [];
34
+ return [{
35
+ operationId,
36
+ repoId: numberValue(row.repoId),
37
+ repoName,
38
+ packageName: stringValue(row.packageName),
39
+ serviceName: stringValue(row.serviceName) ?? '',
40
+ qualifiedName: stringValue(row.qualifiedName) ?? '',
41
+ servicePath,
42
+ operationPath,
43
+ operationName,
44
+ sourceFile: stringValue(row.sourceFile) ?? '',
45
+ sourceLine: numberValue(row.sourceLine) ?? 0,
46
+ score: numberValue(row.score) ?? 0,
47
+ reasons: stringArray(row.reasons),
48
+ }];
49
+ });
50
+ }
51
+
52
+ function queryOperationTargets(
53
+ db: Db,
54
+ effectiveOperationPath: string | undefined,
55
+ originalOperationPath: string | undefined,
56
+ workspaceId: number | undefined,
57
+ ): OperationTarget[] {
58
+ const operationPath = effectiveOperationPath ?? originalOperationPath;
59
+ if (!operationPath) return [];
60
+ if (extractPlaceholders(operationPath).length > 0)
61
+ return templateOperationTargets(db, operationPath, workspaceId);
62
+ return exactOperationTargets(db, operationPath, workspaceId);
63
+ }
64
+
65
+ function exactOperationTargets(
66
+ db: Db,
67
+ operationPath: string,
68
+ workspaceId: number | undefined,
69
+ ): OperationTarget[] {
70
+ const simple = operationPath.replace(/^\//, '').split('.').at(-1) ?? operationPath;
71
+ const rows = recordRows(db.prepare(
72
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
73
+ s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
74
+ o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
75
+ o.source_line sourceLine FROM cds_operations o
76
+ JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
77
+ WHERE (? IS NULL OR r.workspace_id=?)
78
+ AND (o.operation_path IN (?,?) OR o.operation_name=?)
79
+ ORDER BY r.name,s.service_path,o.operation_name,o.id`,
80
+ ).all(workspaceId, workspaceId, operationPath, `/${simple}`, simple));
81
+ return rows.flatMap(targetFromRow);
82
+ }
83
+
84
+ function templateOperationTargets(
85
+ db: Db,
86
+ operationTemplate: string,
87
+ workspaceId: number | undefined,
88
+ ): OperationTarget[] {
89
+ const rows = recordRows(db.prepare(
90
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,
91
+ s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,
92
+ o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,
93
+ o.source_line sourceLine FROM cds_operations o
94
+ JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
95
+ WHERE (? IS NULL OR r.workspace_id=?)
96
+ ORDER BY r.name,s.service_path,o.operation_name,o.id`,
97
+ ).all(workspaceId, workspaceId));
98
+ return rows.flatMap((row) => {
99
+ const operationPath = stringValue(row.operationPath);
100
+ return matchRuntimeTemplate(operationTemplate, operationPath)
101
+ ? targetFromRow(row)
102
+ : [];
103
+ });
104
+ }
105
+
106
+ function targetFromRow(row: Record<string, unknown>): OperationTarget[] {
107
+ const operationId = numberValue(row.operationId);
108
+ const repoName = stringValue(row.repoName);
109
+ const servicePath = stringValue(row.servicePath);
110
+ const operationPath = stringValue(row.operationPath);
111
+ const operationName = stringValue(row.operationName);
112
+ if (operationId === undefined || !repoName || !servicePath || !operationPath || !operationName)
113
+ return [];
114
+ return [{
115
+ operationId,
116
+ repoId: numberValue(row.repoId),
117
+ repoName,
118
+ packageName: stringValue(row.packageName),
119
+ serviceName: stringValue(row.serviceName) ?? '',
120
+ qualifiedName: stringValue(row.qualifiedName) ?? '',
121
+ servicePath,
122
+ operationPath,
123
+ operationName,
124
+ sourceFile: stringValue(row.sourceFile) ?? '',
125
+ sourceLine: numberValue(row.sourceLine) ?? 0,
126
+ score: 0.2,
127
+ reasons: ['operation_path_match'],
128
+ }];
129
+ }
130
+
131
+ function record(value: unknown): Record<string, unknown> {
132
+ return isRecord(value) ? value : {};
133
+ }
134
+
135
+ function recordRows(value: unknown): Array<Record<string, unknown>> {
136
+ return Array.isArray(value) ? value.filter(isRecord) : [];
137
+ }
138
+
139
+ function isRecord(value: unknown): value is Record<string, unknown> {
140
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
141
+ }
142
+
143
+ function stringValue(value: unknown): string | undefined {
144
+ return typeof value === 'string' ? value : undefined;
145
+ }
146
+
147
+ function numberValue(value: unknown): number | undefined {
148
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
149
+ }
150
+
151
+ function stringArray(value: unknown): string[] {
152
+ return Array.isArray(value)
153
+ ? value.filter((item): item is string => typeof item === 'string')
154
+ : [];
155
+ }
@@ -0,0 +1,187 @@
1
+ import type { Db } from '../db/connection.js';
2
+ import { canonicalImplementationEvidence } from '../linker/000-implementation-candidates.js';
3
+ import type { ImplementationHint } from '../types.js';
4
+ import { projectBounded } from '../utils/000-bounded-projection.js';
5
+ import {
6
+ selectImplementation,
7
+ type ImplementationSelection,
8
+ } from './implementation-hints.js';
9
+
10
+ export interface ImplementationGraphEdge {
11
+ status?: string;
12
+ evidence_json?: string;
13
+ }
14
+
15
+ export interface ImplementationSelectionOptions {
16
+ implementationRepo?: string;
17
+ implementationHints?: ImplementationHint[];
18
+ }
19
+
20
+ export function hintedImplementationSelection(
21
+ db: Db,
22
+ edge: ImplementationGraphEdge | undefined,
23
+ operationId: string,
24
+ options: ImplementationSelectionOptions,
25
+ ): ImplementationSelection {
26
+ if (!edge || edge.status !== 'ambiguous')
27
+ return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
28
+ return selectImplementation(
29
+ parsedEvidence(edge.evidence_json), options.implementationHints,
30
+ options.implementationRepo, canonicalImplementationEvidence(db, operationId),
31
+ );
32
+ }
33
+
34
+ export function contextualImplementationSelection(
35
+ db: Db,
36
+ edge: ImplementationGraphEdge | undefined,
37
+ operationId: string,
38
+ callerRepoId: number | undefined,
39
+ remoteEvidence: Record<string, unknown>,
40
+ options: ImplementationSelectionOptions,
41
+ ): ImplementationSelection {
42
+ const hinted = hintedImplementationSelection(db, edge, operationId, options);
43
+ if (hinted.methodId || hinted.blocksAutomatic || !edge
44
+ || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
45
+ const candidates = implementationCandidates(
46
+ canonicalImplementationEvidence(db, operationId) ?? parsedEvidence(edge.evidence_json),
47
+ );
48
+ const scores = candidates.filter((candidate) => candidate.accepted)
49
+ .map((candidate) => contextualScore(candidate, callerRepoId, remoteEvidence))
50
+ .sort(compareScore);
51
+ if (scores.length === 0)
52
+ return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
53
+ const [first, second] = scores;
54
+ if (first?.methodId !== undefined && first.score > 0
55
+ && (!second || first.score > second.score))
56
+ return selectedContext(first, scores);
57
+ return tiedContext(hinted, scores);
58
+ }
59
+
60
+ function selectedContext(
61
+ first: ContextScore,
62
+ scores: ContextScore[],
63
+ ): ImplementationSelection {
64
+ const projection = projectBounded(scores, compareScore);
65
+ return {
66
+ methodId: String(first.methodId),
67
+ blocksAutomatic: false,
68
+ evidence: {
69
+ status: 'selected',
70
+ selectedMethodId: first.methodId,
71
+ candidateScores: projection.items,
72
+ candidateScoreCount: projection.totalCount,
73
+ shownCandidateScoreCount: projection.shownCount,
74
+ omittedCandidateScoreCount: projection.omittedCount,
75
+ },
76
+ };
77
+ }
78
+
79
+ function tiedContext(
80
+ hinted: ImplementationSelection,
81
+ scores: ContextScore[],
82
+ ): ImplementationSelection {
83
+ if (hinted.evidence.reason === 'no_scoped_hint_matched_edge') return hinted;
84
+ const projection = projectBounded(scores, compareScore);
85
+ return {
86
+ blocksAutomatic: false,
87
+ evidence: {
88
+ status: 'tied',
89
+ tieReason: scores.length > 1
90
+ ? 'duplicate_helper_implementation_candidates'
91
+ : 'no_unique_materially_stronger_candidate',
92
+ candidateScores: projection.items,
93
+ candidateScoreCount: projection.totalCount,
94
+ shownCandidateScoreCount: projection.shownCount,
95
+ omittedCandidateScoreCount: projection.omittedCount,
96
+ },
97
+ };
98
+ }
99
+
100
+ interface ImplementationCandidate {
101
+ accepted: boolean;
102
+ methodId?: number;
103
+ score: number;
104
+ handlerPackage: Record<string, unknown>;
105
+ applicationPackage: Record<string, unknown>;
106
+ }
107
+
108
+ interface ContextScore {
109
+ methodId?: number;
110
+ score: number;
111
+ reasons: string[];
112
+ handlerPackage: Record<string, unknown>;
113
+ applicationPackage: Record<string, unknown>;
114
+ }
115
+
116
+ function implementationCandidates(evidence: Record<string, unknown>): ImplementationCandidate[] {
117
+ const rows = Array.isArray(evidence.candidates) ? evidence.candidates : [];
118
+ return rows.flatMap((value) => {
119
+ const row = record(value);
120
+ return Object.keys(row).length === 0 ? [] : [{
121
+ accepted: row.accepted === true,
122
+ methodId: numberValue(row.methodId),
123
+ score: numberValue(row.score) ?? 0,
124
+ handlerPackage: record(row.handlerPackage),
125
+ applicationPackage: record(row.applicationPackage),
126
+ }];
127
+ });
128
+ }
129
+
130
+ function contextualScore(
131
+ candidate: ImplementationCandidate,
132
+ callerRepoId: number,
133
+ remoteEvidence: Record<string, unknown>,
134
+ ): ContextScore {
135
+ const reasons: string[] = [];
136
+ let score = candidate.score;
137
+ if (numberValue(candidate.handlerPackage.id) === callerRepoId) {
138
+ score += 10;
139
+ reasons.push('handler_package_matches_caller_repository');
140
+ }
141
+ if (numberValue(candidate.applicationPackage.id) === callerRepoId) {
142
+ score += 10;
143
+ reasons.push('registration_package_matches_caller_repository');
144
+ }
145
+ if (hasRemoteContext(remoteEvidence)) {
146
+ score += 1;
147
+ reasons.push('remote_call_context_available');
148
+ }
149
+ return {
150
+ methodId: candidate.methodId,
151
+ score,
152
+ reasons,
153
+ handlerPackage: candidate.handlerPackage,
154
+ applicationPackage: candidate.applicationPackage,
155
+ };
156
+ }
157
+
158
+ function hasRemoteContext(evidence: Record<string, unknown>): boolean {
159
+ return typeof evidence.effectiveServicePath === 'string'
160
+ || typeof evidence.effectiveDestination === 'string'
161
+ || typeof evidence.effectiveAlias === 'string';
162
+ }
163
+
164
+ function compareScore(left: ContextScore, right: ContextScore): number {
165
+ return right.score - left.score
166
+ || Number(left.methodId ?? 0) - Number(right.methodId ?? 0);
167
+ }
168
+
169
+ function parsedEvidence(value: string | undefined): Record<string, unknown> {
170
+ try {
171
+ return record(JSON.parse(String(value ?? '{}')) as unknown);
172
+ } catch {
173
+ return {};
174
+ }
175
+ }
176
+
177
+ function record(value: unknown): Record<string, unknown> {
178
+ return isRecord(value) ? value : {};
179
+ }
180
+
181
+ function isRecord(value: unknown): value is Record<string, unknown> {
182
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
183
+ }
184
+
185
+ function numberValue(value: unknown): number | undefined {
186
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
187
+ }
@@ -0,0 +1,30 @@
1
+ import { projectBounded } from '../utils/000-bounded-projection.js';
2
+
3
+ export function boundedContextCandidates(values: unknown[]): {
4
+ candidates: Array<Record<string, unknown>>;
5
+ candidateCount: number;
6
+ shownCandidateCount: number;
7
+ omittedCandidateCount: number;
8
+ } {
9
+ const candidates = values.flatMap((value): Array<Record<string, unknown>> => {
10
+ return isRecord(value) ? [value] : [];
11
+ });
12
+ const projection = projectBounded(candidates, (left, right) =>
13
+ Number(right.score ?? 0) - Number(left.score ?? 0)
14
+ || String(left.repoName ?? '').localeCompare(String(right.repoName ?? ''))
15
+ || String(left.servicePath ?? '').localeCompare(String(right.servicePath ?? ''))
16
+ || String(left.sourceFile ?? '').localeCompare(String(right.sourceFile ?? ''))
17
+ || Number(left.sourceLine ?? 0) - Number(right.sourceLine ?? 0)
18
+ || Number(left.bindingId ?? left.operationId ?? 0)
19
+ - Number(right.bindingId ?? right.operationId ?? 0));
20
+ return {
21
+ candidates: projection.items,
22
+ candidateCount: projection.totalCount,
23
+ shownCandidateCount: projection.shownCount,
24
+ omittedCandidateCount: projection.omittedCount,
25
+ };
26
+ }
27
+
28
+ function isRecord(value: unknown): value is Record<string, unknown> {
29
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
30
+ }
@@ -0,0 +1,61 @@
1
+ interface ImplementationStartEdge {
2
+ id: number;
3
+ status?: string;
4
+ }
5
+
6
+ export function implementationStartDiagnostic(
7
+ edge: ImplementationStartEdge,
8
+ evidence: Record<string, unknown>,
9
+ ): Record<string, unknown> {
10
+ return {
11
+ severity: 'warning',
12
+ code: edge.status === 'ambiguous'
13
+ ? 'trace_start_ambiguous'
14
+ : 'trace_start_implementation_unresolved',
15
+ message: `Indexed operation matched but implementation edge is ${String(
16
+ edge.status ?? 'unresolved',
17
+ )}`,
18
+ resolutionStage: 'implementation',
19
+ resolutionStatus: edge.status === 'ambiguous'
20
+ ? 'ambiguous_implementation'
21
+ : 'rejected_implementation',
22
+ implementationEdgeId: edge.id,
23
+ implementationStatus: edge.status,
24
+ implementationAmbiguityReasons: evidence.ambiguityReasons,
25
+ implementationRejectionReasons: implementationRejectionReasons(evidence),
26
+ implementationHintSuggestions: evidence.implementationHintSuggestions,
27
+ implementationHintSuggestionCount: evidence.implementationHintSuggestionCount,
28
+ shownImplementationHintSuggestionCount:
29
+ evidence.shownImplementationHintSuggestionCount,
30
+ omittedImplementationHintSuggestionCount:
31
+ evidence.omittedImplementationHintSuggestionCount,
32
+ candidates: evidence.candidates,
33
+ candidateCount: evidence.candidateCount,
34
+ shownCandidateCount: evidence.shownCandidateCount,
35
+ omittedCandidateCount: evidence.omittedCandidateCount,
36
+ };
37
+ }
38
+
39
+ function implementationRejectionReasons(
40
+ evidence: Record<string, unknown>,
41
+ ): string[] {
42
+ const candidates = recordArray(evidence.candidates);
43
+ const reasons = candidates.flatMap((candidate) => stringArray(candidate.rejectedReasons));
44
+ return [...new Set(reasons)].sort();
45
+ }
46
+
47
+ function recordArray(value: unknown): Array<Record<string, unknown>> {
48
+ return Array.isArray(value)
49
+ ? value.filter(isRecord)
50
+ : [];
51
+ }
52
+
53
+ function stringArray(value: unknown): string[] {
54
+ return Array.isArray(value)
55
+ ? value.filter((item): item is string => typeof item === 'string')
56
+ : [];
57
+ }
58
+
59
+ function isRecord(value: unknown): value is Record<string, unknown> {
60
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
61
+ }