@saptools/service-flow 0.1.64 → 0.1.66

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 (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +67 -11
  3. package/TECHNICAL-NOTE.md +41 -1
  4. package/dist/{chunk-TBH33OYC.js → chunk-TOVX4WYH.js} +3765 -643
  5. package/dist/chunk-TOVX4WYH.js.map +1 -0
  6. package/dist/cli.js +231 -292
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +196 -1
  9. package/dist/index.js +7 -3
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/002-doctor-lifecycle.ts +66 -0
  13. package/src/cli/doctor.ts +14 -27
  14. package/src/cli.ts +130 -102
  15. package/src/db/000-call-fact-repository.ts +537 -0
  16. package/src/db/001-fact-lifecycle.ts +111 -0
  17. package/src/db/migrations.ts +16 -1
  18. package/src/db/repositories.ts +1 -315
  19. package/src/db/schema.ts +4 -2
  20. package/src/index.ts +22 -0
  21. package/src/linker/004-event-subscription-handler-linker.ts +300 -0
  22. package/src/linker/cross-repo-linker.ts +23 -2
  23. package/src/output/001-compact-json-output.ts +6 -0
  24. package/src/parsers/imported-wrapper-parser.ts +4 -0
  25. package/src/parsers/outbound-call-parser.ts +11 -1
  26. package/src/parsers/symbol-parser.ts +110 -2
  27. package/src/trace/010-traversal-scope.ts +188 -0
  28. package/src/trace/011-event-subscriber-traversal.ts +366 -0
  29. package/src/trace/012-trace-graph-lookups.ts +74 -0
  30. package/src/trace/013-trace-root-scopes.ts +279 -0
  31. package/src/trace/014-compact-contract.ts +347 -0
  32. package/src/trace/015-trace-edge-recorder.ts +336 -0
  33. package/src/trace/016-compact-projector.ts +697 -0
  34. package/src/trace/017-trace-context.ts +378 -0
  35. package/src/trace/018-compact-trace.ts +87 -0
  36. package/src/trace/019-trace-edge-semantics.ts +328 -0
  37. package/src/trace/020-compact-field-projection.ts +387 -0
  38. package/src/trace/dynamic-branches.ts +35 -13
  39. package/src/trace/trace-engine.ts +271 -245
  40. package/src/types.ts +10 -0
  41. package/src/version.ts +1 -1
  42. package/dist/chunk-TBH33OYC.js.map +0 -1
@@ -0,0 +1,387 @@
1
+ import { redactText } from '../utils/redaction.js';
2
+ import type {
3
+ ImplementationHint,
4
+ TraceOptions,
5
+ TraceStart,
6
+ } from '../types.js';
7
+ import { compareBinary } from './010-traversal-scope.js';
8
+ import type {
9
+ CompactDecisionInput,
10
+ CompactDecisionV1,
11
+ CompactDiagnosticDetailsV1,
12
+ CompactDiagnosticRowV1,
13
+ CompactEdgeObservation,
14
+ CompactHintV1,
15
+ CompactProjectedDiagnostic,
16
+ CompactQueryV1,
17
+ CompactStartV1,
18
+ CompactStatusCountsV1,
19
+ } from './014-compact-contract.js';
20
+
21
+ const compactNameLimit = 8;
22
+ const compactDiagnosticMessages: Readonly<Record<string, string>> = {
23
+ schema_upgrade_required: 'The database schema must be upgraded before tracing.',
24
+ reindex_required: 'Current analyzer facts are required before tracing.',
25
+ trace_workspace_ambiguous: 'The trace workspace is ambiguous.',
26
+ trace_runtime_variables_missing: 'Runtime variable names are required to resolve a branch.',
27
+ implementation_hint_mismatch: 'The implementation hint did not select one implementation.',
28
+ selected_handler_provenance_mismatch: 'Selected handler provenance did not match its graph target.',
29
+ selected_handler_target_not_found: 'The selected handler target is not indexed.',
30
+ trace_start_implementation_unresolved: 'The trace start implementation is unresolved.',
31
+ };
32
+
33
+ export function projectCompactDecision(
34
+ input: CompactDecisionInput | undefined,
35
+ ): CompactDecisionV1 {
36
+ if (!input) return {};
37
+ const out = resolutionDecision(input);
38
+ addNameDecision(out, input);
39
+ addDynamicDecision(out, input);
40
+ addImplementationDecision(out, input);
41
+ addEventDecision(out, input);
42
+ const reasonCode = compactSafeCode(input.reasonCode);
43
+ if (reasonCode) out.reasonCode = reasonCode;
44
+ addRemediationDecision(out, input);
45
+ return out;
46
+ }
47
+
48
+ function resolutionDecision(input: CompactDecisionInput): CompactDecisionV1 {
49
+ const out: CompactDecisionV1 = {};
50
+ const status = compactSafeCode(input.effectiveResolutionStatus);
51
+ if (status) out.effectiveResolutionStatus = status;
52
+ if (!persistedResolutionDiffers(input)) return out;
53
+ const persistedStatus = compactSafeCode(input.persistedResolutionStatus);
54
+ if (persistedStatus) out.persistedResolutionStatus = persistedStatus;
55
+ return out;
56
+ }
57
+
58
+ function addNameDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
59
+ const allNames = safeVariableNames(input.missingVariableNames);
60
+ const names = allNames.slice(0, compactNameLimit);
61
+ const total = Math.max(compactCount(input.missingVariableCount), allNames.length);
62
+ if (names.length > 0) out.missingVariableNames = names;
63
+ if (total === 0) return;
64
+ out.missingVariableCount = total;
65
+ out.shownMissingVariableCount = names.length;
66
+ out.omittedMissingVariableCount = Math.max(0, total - names.length);
67
+ }
68
+
69
+ function addDynamicDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
70
+ if (input.dynamicMode) out.dynamicMode = input.dynamicMode;
71
+ if (input.candidateCount !== undefined) out.candidateCount = compactCount(input.candidateCount);
72
+ if (input.viableCandidateCount !== undefined)
73
+ out.viableCandidateCount = compactCount(input.viableCandidateCount);
74
+ if (input.rejectedCandidateCount !== undefined)
75
+ out.rejectedCandidateCount = compactCount(input.rejectedCandidateCount);
76
+ if (input.omittedCandidateCount !== undefined)
77
+ out.omittedCandidateCount = compactCount(input.omittedCandidateCount);
78
+ }
79
+
80
+ function addImplementationDecision(
81
+ out: CompactDecisionV1,
82
+ input: CompactDecisionInput,
83
+ ): void {
84
+ const strategy = compactSafeCode(input.implementationStrategy);
85
+ if (strategy) out.implementationStrategy = strategy;
86
+ if (input.implementationGuided !== undefined)
87
+ out.implementationGuided = input.implementationGuided;
88
+ if (input.implementationContextual !== undefined)
89
+ out.implementationContextual = input.implementationContextual;
90
+ }
91
+
92
+ function addEventDecision(out: CompactDecisionV1, input: CompactDecisionInput): void {
93
+ addEventCodes(out, input);
94
+ if (input.eventSubscriptionCount !== undefined)
95
+ out.eventSubscriptionCount = compactCount(input.eventSubscriptionCount);
96
+ if (input.roleSiteMatchCount !== undefined)
97
+ out.roleSiteMatchCount = compactCount(input.roleSiteMatchCount);
98
+ }
99
+
100
+ function addEventCodes(out: CompactDecisionV1, input: CompactDecisionInput): void {
101
+ const values: Array<[keyof CompactDecisionV1, string | undefined]> = [
102
+ ['eventMatchStrategy', compactSafeCode(input.eventMatchStrategy)],
103
+ ['dispatchCertainty', compactSafeCode(input.dispatchCertainty)],
104
+ ['associationStatus', compactSafeCode(input.associationStatus)],
105
+ ['associationBasis', compactSafeCode(input.associationBasis)],
106
+ ['eventScope', compactSafeCode(input.eventScope)],
107
+ ['callRole', compactSafeCode(input.callRole)],
108
+ ['factOrigin', compactSafeCode(input.factOrigin)],
109
+ ['bodyExpansion', compactSafeCode(input.bodyExpansion)],
110
+ ];
111
+ for (const [key, value] of values) {
112
+ if (value) Object.assign(out, { [key]: value });
113
+ }
114
+ }
115
+
116
+ function addRemediationDecision(
117
+ out: CompactDecisionV1,
118
+ input: CompactDecisionInput,
119
+ ): void {
120
+ const hint = input.remediationCode
121
+ ? compactRemediationHint(input.remediationCode) : undefined;
122
+ if (!hint) return;
123
+ out.remediationHint = hint;
124
+ const total = Math.max(1, compactCount(input.remediationHintCount));
125
+ out.omittedRemediationHintCount = Math.max(0, total - 1);
126
+ }
127
+
128
+ export function projectCompactDiagnostics(
129
+ values: Array<Record<string, unknown>>,
130
+ ): CompactProjectedDiagnostic[] {
131
+ return values.map((value, index) => compactDiagnostic(value, index))
132
+ .sort(compareCompactDiagnostic);
133
+ }
134
+
135
+ export function projectCompactStart(start: TraceStart): CompactStartV1 {
136
+ return {
137
+ repo: start.repo ?? null,
138
+ servicePath: start.servicePath ?? null,
139
+ operation: start.operation ?? null,
140
+ operationPath: start.operationPath ?? null,
141
+ handler: start.handler ?? null,
142
+ };
143
+ }
144
+
145
+ export function projectCompactQuery(options: TraceOptions): CompactQueryV1 {
146
+ const hints = (options.implementationHints ?? []).map(projectCompactHint)
147
+ .sort((left, right) => compareBinary(
148
+ JSON.stringify(left), JSON.stringify(right),
149
+ ));
150
+ return {
151
+ depth: compactPositiveInteger(options.depth) ?? 25,
152
+ includeAsync: Boolean(options.includeAsync),
153
+ includeDb: Boolean(options.includeDb),
154
+ includeExternal: Boolean(options.includeExternal),
155
+ dynamicMode: options.dynamicMode ?? 'strict',
156
+ maxDynamicCandidates: compactPositiveInteger(options.maxDynamicCandidates) ?? 5,
157
+ suppliedVariableNames: compactSortedUnique(Object.keys(options.vars ?? {})),
158
+ runtimeValuesOmitted: true,
159
+ implementationRepo: options.implementationRepo ?? null,
160
+ implementationHints: hints,
161
+ };
162
+ }
163
+
164
+ function projectCompactHint(hint: ImplementationHint): CompactHintV1 {
165
+ return {
166
+ servicePath: hint.servicePath ?? null,
167
+ operationPath: hint.operationPath ?? null,
168
+ packageName: hint.packageName ?? null,
169
+ repositoryName: hint.repositoryName ?? null,
170
+ candidateFamily: hint.candidateFamily ?? null,
171
+ implementationRepo: hint.implementationRepo ?? null,
172
+ };
173
+ }
174
+
175
+ export function compactStatusCounts(
176
+ values: CompactEdgeObservation[],
177
+ ): CompactStatusCountsV1 {
178
+ const counts: CompactStatusCountsV1 = {
179
+ resolved: 0, terminal: 0, inferred: 0, dynamic: 0,
180
+ ambiguous: 0, unresolved: 0, cycle: 0,
181
+ };
182
+ for (const value of values) counts[value.status] += 1;
183
+ return counts;
184
+ }
185
+
186
+ export function compactCompleteness(
187
+ counts: CompactStatusCountsV1,
188
+ diagnostics: CompactDiagnosticRowV1[],
189
+ ): 'complete' | 'partial' | 'blocked' {
190
+ const total = compactStatusTotal(counts);
191
+ if (total === 0 && diagnostics.some(compactBlockingDiagnostic)) return 'blocked';
192
+ if (counts.dynamic + counts.ambiguous + counts.unresolved > 0) return 'partial';
193
+ if (diagnostics.some((item) => item[1] === 'error' || item[1] === 'warning'))
194
+ return 'partial';
195
+ return 'complete';
196
+ }
197
+
198
+ export function compactStatusTotal(counts: CompactStatusCountsV1): number {
199
+ return counts.resolved + counts.terminal + counts.inferred + counts.dynamic
200
+ + counts.ambiguous + counts.unresolved + counts.cycle;
201
+ }
202
+
203
+ export function removeEquivalentCompactPersistedDecision(
204
+ decision: CompactDecisionV1,
205
+ ): void {
206
+ if (decision.persistedResolutionStatus !== decision.effectiveResolutionStatus) return;
207
+ if (!decision.persistedTarget || decision.persistedTarget !== decision.effectiveTarget) return;
208
+ delete decision.persistedResolutionStatus;
209
+ delete decision.persistedTarget;
210
+ }
211
+
212
+ function compactBlockingDiagnostic(item: CompactDiagnosticRowV1): boolean {
213
+ if (item[1] === 'error') return true;
214
+ return item[2] === 'schema_upgrade_required'
215
+ || item[2] === 'reindex_required'
216
+ || item[2] === 'trace_workspace_ambiguous'
217
+ || item[2].startsWith('selector_')
218
+ || item[2].startsWith('trace_start_');
219
+ }
220
+
221
+ function compactDiagnostic(
222
+ value: Record<string, unknown>,
223
+ index: number,
224
+ ): CompactProjectedDiagnostic {
225
+ const code = compactSafeCode(value.code) ?? 'unknown_diagnostic';
226
+ const details = compactDiagnosticDetails(value, code);
227
+ return {
228
+ index,
229
+ severity: compactDiagnosticSeverity(value.severity),
230
+ code,
231
+ message: compactDiagnosticMessages[code] ?? `See detailed diagnostic at index ${index}.`,
232
+ file: compactSafeSourceFile(value.sourceFile) ?? compactSafeSourceFile(value.file),
233
+ line: compactPositiveInteger(value.sourceLine) ?? compactPositiveInteger(value.line),
234
+ details: Object.keys(details).length > 0 ? details : undefined,
235
+ };
236
+ }
237
+
238
+ function compactDiagnosticDetails(
239
+ value: Record<string, unknown>,
240
+ code: string,
241
+ ): CompactDiagnosticDetailsV1 {
242
+ const out: CompactDiagnosticDetailsV1 = {};
243
+ const reasonCode = compactSafeCode(value.reasonCode);
244
+ if (reasonCode) out.reasonCode = reasonCode;
245
+ addDiagnosticNames(out, value);
246
+ addDiagnosticCounts(out, value);
247
+ const hint = compactDiagnosticRemediation(code);
248
+ if (hint) {
249
+ out.remediationHint = hint;
250
+ out.omittedHintCount = Math.max(
251
+ 0, compactDiagnosticHintCount(value, code, out) - 1,
252
+ );
253
+ }
254
+ return out;
255
+ }
256
+
257
+ function addDiagnosticNames(
258
+ out: CompactDiagnosticDetailsV1,
259
+ value: Record<string, unknown>,
260
+ ): void {
261
+ const allNames = safeVariableNames(compactStringArray(value.missingVariables));
262
+ const names = allNames.slice(0, compactNameLimit);
263
+ const total = Math.max(allNames.length, compactCount(value.missingVariableCount));
264
+ if (names.length > 0) out.missingVariableNames = names;
265
+ if (total === 0) return;
266
+ out.missingVariableCount = total;
267
+ out.shownMissingVariableCount = names.length;
268
+ out.omittedMissingVariableCount = Math.max(0, total - names.length);
269
+ }
270
+
271
+ function addDiagnosticCounts(
272
+ out: CompactDiagnosticDetailsV1,
273
+ value: Record<string, unknown>,
274
+ ): void {
275
+ if (value.candidateCount !== undefined)
276
+ out.candidateCount = compactCount(value.candidateCount);
277
+ if (value.viableCandidateCount !== undefined)
278
+ out.viableCandidateCount = compactCount(value.viableCandidateCount);
279
+ if (value.rejectedCandidateCount !== undefined)
280
+ out.rejectedCandidateCount = compactCount(value.rejectedCandidateCount);
281
+ }
282
+
283
+ function compareCompactDiagnostic(
284
+ left: CompactProjectedDiagnostic,
285
+ right: CompactProjectedDiagnostic,
286
+ ): number {
287
+ const ranks = { error: 0, warning: 1, info: 2 } as const;
288
+ return ranks[left.severity] - ranks[right.severity]
289
+ || compareBinary(left.code, right.code)
290
+ || compareBinary(left.file ?? '', right.file ?? '')
291
+ || (left.line ?? Number.MAX_SAFE_INTEGER) - (right.line ?? Number.MAX_SAFE_INTEGER)
292
+ || compareBinary(left.message, right.message)
293
+ || left.index - right.index;
294
+ }
295
+
296
+ function persistedResolutionDiffers(input: CompactDecisionInput): boolean {
297
+ return input.persistedResolutionStatus !== undefined
298
+ && (input.persistedResolutionStatus !== input.effectiveResolutionStatus
299
+ || JSON.stringify(input.persistedTarget) !== JSON.stringify(input.effectiveTarget));
300
+ }
301
+
302
+ function compactDiagnosticSeverity(value: unknown): 'error' | 'warning' | 'info' {
303
+ return value === 'error' || value === 'warning' ? value : 'info';
304
+ }
305
+
306
+ function compactDiagnosticRemediation(code: string): string | undefined {
307
+ if (code === 'schema_upgrade_required' || code === 'reindex_required')
308
+ return compactRemediationHint('reindex_and_link');
309
+ if (code === 'trace_runtime_variables_missing')
310
+ return compactRemediationHint('provide_runtime_variables');
311
+ if (code === 'implementation_hint_mismatch')
312
+ return compactRemediationHint('select_implementation');
313
+ return undefined;
314
+ }
315
+
316
+ function compactDiagnosticHintCount(
317
+ value: Record<string, unknown>,
318
+ code: string,
319
+ details: CompactDiagnosticDetailsV1,
320
+ ): number {
321
+ const missing = code === 'trace_runtime_variables_missing'
322
+ ? details.missingVariableCount ?? 0 : 0;
323
+ return Math.max(1, missing, compactArrayLength(value.suggestions),
324
+ compactArrayLength(value.implementationHintSuggestions),
325
+ compactArrayLength(value.copyableExamples), compactCount(value.suggestionCount),
326
+ compactCount(value.implementationHintSuggestionCount),
327
+ compactCount(value.copyableExampleCount));
328
+ }
329
+
330
+ function compactRemediationHint(code: string): string | undefined {
331
+ if (code === 'provide_runtime_variables') return 'Provide the missing variable names listed in details.';
332
+ if (code === 'select_implementation') return 'Select one implementation with a scoped implementation hint.';
333
+ if (code === 'reindex_and_link') return 'Force reindex, then force relink the workspace.';
334
+ if (code === 'inspect_detailed_edge') return 'Inspect the correlated detailed trace edge.';
335
+ return undefined;
336
+ }
337
+
338
+ export function compactSafeCode(value: unknown): string | undefined {
339
+ return typeof value === 'string' && /^[a-z][a-z0-9_.-]{0,79}$/.test(value)
340
+ ? value : undefined;
341
+ }
342
+
343
+ export function projectCompactDecisionTarget(
344
+ kindValue: string,
345
+ id: string,
346
+ ): string | undefined {
347
+ const kind = compactSafeCode(kindValue);
348
+ if (!kind || id.length === 0 || id.length > 240 || /[\r\n]/.test(id)) return undefined;
349
+ if (/^[a-z]+:\/\//i.test(id)
350
+ || /\b(?:bearer|token|secret|password|credential|authorization)\b/i.test(id))
351
+ return undefined;
352
+ const redacted = redactText(id);
353
+ return redacted === id ? `${kind}:${redacted}` : undefined;
354
+ }
355
+
356
+ function compactSafeSourceFile(value: unknown): string | undefined {
357
+ return typeof value === 'string' && value.length > 0 && value.length <= 512
358
+ && !/^[a-z]+:\/\//i.test(value) && !/[\r\n]/.test(value) ? value : undefined;
359
+ }
360
+
361
+ function safeVariableNames(values: string[] | undefined): string[] {
362
+ return compactSortedUnique((values ?? [])
363
+ .filter((value) => /^[A-Za-z_$][\w$]*$/.test(value)));
364
+ }
365
+
366
+ function compactCount(value: unknown): number {
367
+ return typeof value === 'number' && Number.isFinite(value)
368
+ ? Math.max(0, Math.floor(value)) : 0;
369
+ }
370
+
371
+ function compactPositiveInteger(value: unknown): number | undefined {
372
+ const normalized = compactCount(value);
373
+ return normalized > 0 ? normalized : undefined;
374
+ }
375
+
376
+ function compactStringArray(value: unknown): string[] {
377
+ return Array.isArray(value)
378
+ ? value.filter((item): item is string => typeof item === 'string') : [];
379
+ }
380
+
381
+ function compactArrayLength(value: unknown): number {
382
+ return Array.isArray(value) ? value.length : 0;
383
+ }
384
+
385
+ function compactSortedUnique(values: string[]): string[] {
386
+ return [...new Set(values)].sort(compareBinary);
387
+ }
@@ -6,26 +6,40 @@ interface DynamicBranchCall {
6
6
  source_line: number;
7
7
  }
8
8
 
9
+ export interface DynamicCandidateBranch {
10
+ edge: TraceEdge;
11
+ operationId?: number;
12
+ repositoryId?: number;
13
+ servicePath?: string;
14
+ operationPath?: string;
15
+ }
16
+
9
17
  export function dynamicCandidateBranches(
10
18
  depth: number,
11
19
  call: DynamicBranchCall,
12
20
  evidence: Record<string, unknown>,
13
- ): TraceEdge[] {
21
+ ): DynamicCandidateBranch[] {
14
22
  const exploration = objectRecord(evidence.dynamicTargetExploration);
15
23
  return recordArray(evidence.dynamicTargetCandidateSuggestions).map((candidate) => ({
16
- step: depth,
17
- type: 'dynamic_candidate_branch',
18
- from: `${call.repoName}:${call.source_file}:${call.source_line}`,
19
- to: `${String(candidate.servicePath ?? '')}${String(candidate.operationPath ?? '')}`,
20
- evidence: {
21
- ...candidate,
22
- exploratory: true,
23
- dynamicMode: String(exploration.mode ?? 'candidates'),
24
- selected: false,
25
- omittedCandidateCount: numericValue(exploration.omittedCandidateCount),
24
+ edge: {
25
+ step: depth,
26
+ type: 'dynamic_candidate_branch',
27
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
28
+ to: `${String(candidate.servicePath ?? '')}${String(candidate.operationPath ?? '')}`,
29
+ evidence: {
30
+ ...candidate,
31
+ exploratory: true,
32
+ dynamicMode: String(exploration.mode ?? 'candidates'),
33
+ selected: false,
34
+ omittedCandidateCount: numericValue(exploration.omittedCandidateCount),
35
+ },
36
+ confidence: numericValue(candidate.score),
37
+ unresolvedReason: 'Exploratory dynamic target candidate; provide runtime variables to select it',
26
38
  },
27
- confidence: numericValue(candidate.score),
28
- unresolvedReason: 'Exploratory dynamic target candidate; provide runtime variables to select it',
39
+ operationId: optionalNumber(candidate.candidateOperationId),
40
+ repositoryId: optionalNumber(candidate.repoId),
41
+ servicePath: optionalString(candidate.servicePath),
42
+ operationPath: optionalString(candidate.operationPath),
29
43
  }));
30
44
  }
31
45
 
@@ -43,3 +57,11 @@ function recordArray(value: unknown): Record<string, unknown>[] {
43
57
  function numericValue(value: unknown): number {
44
58
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
45
59
  }
60
+
61
+ function optionalNumber(value: unknown): number | undefined {
62
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
63
+ }
64
+
65
+ function optionalString(value: unknown): string | undefined {
66
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
67
+ }