@saptools/service-flow 0.1.50 → 0.1.51

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.
package/dist/index.d.ts CHANGED
@@ -135,6 +135,18 @@ interface ImplementationHint {
135
135
  candidateFamily?: string;
136
136
  implementationRepo: string;
137
137
  }
138
+ type DynamicMode = 'strict' | 'candidates' | 'infer';
139
+ interface TraceOptions {
140
+ depth: number;
141
+ vars?: Record<string, string>;
142
+ includeExternal?: boolean;
143
+ includeDb?: boolean;
144
+ includeAsync?: boolean;
145
+ implementationRepo?: string;
146
+ implementationHints?: ImplementationHint[];
147
+ dynamicMode?: DynamicMode;
148
+ maxDynamicCandidates?: number;
149
+ }
138
150
  interface TraceEdge {
139
151
  step: number;
140
152
  type: string;
@@ -213,19 +225,11 @@ declare function applyVariables(template: string | undefined, vars: Record<strin
213
225
  declare function extractPlaceholders(template: string | undefined): string[];
214
226
  declare function substituteVariables(template: string | undefined, vars: Record<string, string>): RuntimeSubstitution;
215
227
 
216
- declare function trace(db: Db, start: TraceStart, options: {
217
- depth: number;
218
- vars?: Record<string, string>;
219
- includeExternal?: boolean;
220
- includeDb?: boolean;
221
- includeAsync?: boolean;
222
- implementationRepo?: string;
223
- implementationHints?: ImplementationHint[];
224
- }): TraceResult;
228
+ declare function trace(db: Db, start: TraceStart, options: TraceOptions): TraceResult;
225
229
 
226
230
  declare function parseImplementationHint(value: string): ImplementationHint;
227
231
 
228
232
  declare function redactText(text: string): string;
229
233
  declare function redactValue(value: unknown): unknown;
230
234
 
231
- export { type ImplementationHint, type RuntimeSubstitution, applyVariables, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace };
235
+ export { type DynamicMode, type ImplementationHint, type RuntimeSubstitution, type TraceOptions, applyVariables, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace };
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-52OUS3MO.js";
20
+ } from "./chunk-YZJKE5UX.js";
21
21
 
22
22
  // src/parsers/generated-constants-parser.ts
23
23
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/cli.ts CHANGED
@@ -29,6 +29,7 @@ import { renderTraceJson, renderJson } from './output/json-output.js';
29
29
  import { renderDoctorDiagnostics } from './output/doctor-output.js';
30
30
  import { renderMermaid } from './output/mermaid-output.js';
31
31
  import { VERSION } from './version.js';
32
+ import type { DynamicMode } from './types.js';
32
33
  async function init(
33
34
  workspace: string,
34
35
  options: { db?: string; ignore?: string[] },
@@ -155,6 +156,8 @@ export function createProgram(): Command {
155
156
  .option('--implementation-repo <name>')
156
157
  .option('--implementation-hint <scope>', 'scoped implementation hint', collect, [])
157
158
  .option('--var <key=value>', 'dynamic variable', collect, [])
159
+ .option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
160
+ .option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
158
161
  .action(
159
162
  (opts: {
160
163
  workspace?: string;
@@ -171,6 +174,8 @@ export function createProgram(): Command {
171
174
  implementationRepo?: string;
172
175
  implementationHint: string[];
173
176
  var: string[];
177
+ dynamicMode: string;
178
+ maxDynamicCandidates: string;
174
179
  }) =>
175
180
  void withReadOnlyWorkspace(opts.workspace, (db) => {
176
181
  const result = trace(
@@ -190,6 +195,8 @@ export function createProgram(): Command {
190
195
  includeAsync: Boolean(opts.includeAsync),
191
196
  implementationRepo: opts.implementationRepo,
192
197
  implementationHints: opts.implementationHint.map(parseImplementationHint),
198
+ dynamicMode: parseDynamicMode(opts.dynamicMode),
199
+ maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5),
193
200
  },
194
201
  );
195
202
  process.stdout.write(
@@ -299,6 +306,8 @@ export function createProgram(): Command {
299
306
  .option('--implementation-repo <name>')
300
307
  .option('--implementation-hint <scope>', 'scoped implementation hint', collect, [])
301
308
  .option('--var <key=value>', 'dynamic variable', collect, [])
309
+ .option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
310
+ .option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
302
311
  .action(
303
312
  (opts: {
304
313
  workspace?: string;
@@ -310,6 +319,8 @@ export function createProgram(): Command {
310
319
  implementationRepo?: string;
311
320
  implementationHint: string[];
312
321
  var: string[];
322
+ dynamicMode: string;
323
+ maxDynamicCandidates: string;
313
324
  }) =>
314
325
  void withReadOnlyWorkspace(opts.workspace, (db) => {
315
326
  const result = trace(
@@ -328,6 +339,8 @@ export function createProgram(): Command {
328
339
  vars: parseVars(opts.var),
329
340
  implementationRepo: opts.implementationRepo,
330
341
  implementationHints: opts.implementationHint.map(parseImplementationHint),
342
+ dynamicMode: parseDynamicMode(opts.dynamicMode),
343
+ maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5),
331
344
  },
332
345
  );
333
346
  process.stdout.write(
@@ -418,6 +431,15 @@ function collect(value: string, previous: string[]): string[] {
418
431
  previous.push(value);
419
432
  return previous;
420
433
  }
434
+ function parseDynamicMode(value: string | undefined): DynamicMode {
435
+ if (value === undefined || value === 'strict') return 'strict';
436
+ if (value === 'candidates' || value === 'infer') return value;
437
+ throw new Error(`Invalid --dynamic-mode ${value}; expected strict, candidates, or infer`);
438
+ }
439
+ function parsePositiveInteger(value: string | undefined, fallback: number): number {
440
+ const parsed = Number(value);
441
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
442
+ }
421
443
  function fail(error: unknown): void {
422
444
  process.stderr.write(
423
445
  `${error instanceof Error ? error.message : String(error)}\n`,
package/src/index.ts CHANGED
@@ -11,5 +11,5 @@ export { applyVariables, extractPlaceholders, substituteVariables } from './link
11
11
  export type { RuntimeSubstitution } from './linker/dynamic-edge-resolver.js';
12
12
  export { trace } from './trace/trace-engine.js';
13
13
  export { parseImplementationHint } from './trace/implementation-hints.js';
14
- export type { ImplementationHint } from './types.js';
14
+ export type { DynamicMode, ImplementationHint, TraceOptions } from './types.js';
15
15
  export { redactValue, redactText } from './utils/redaction.js';
@@ -224,7 +224,15 @@ function compactCandidateScores(candidates: unknown[]): Array<Record<string, unk
224
224
  return candidates.flatMap((candidate): Array<Record<string, unknown>> => {
225
225
  const row = objectValue(candidate);
226
226
  if (!row) return [];
227
- return [{ repo: row.repoName, servicePath: row.servicePath, operationPath: row.operationPath, score: row.score, reasons: row.reasons }];
227
+ return [{
228
+ repo: row.repoName,
229
+ servicePath: row.servicePath,
230
+ operationPath: row.operationPath,
231
+ score: row.score,
232
+ reasons: Array.isArray(row.reasons)
233
+ ? row.reasons.filter((reason): reason is string => typeof reason === 'string')
234
+ : ['operation_path_match'],
235
+ }];
228
236
  });
229
237
  }
230
238
 
@@ -26,9 +26,9 @@ function rows(
26
26
  workspaceId?: number,
27
27
  ): OperationTarget[] {
28
28
  const names = operationLookupNames(operationPath);
29
- return db
29
+ const result = db
30
30
  .prepare(
31
- `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
31
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score
32
32
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
33
33
  WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path IN (?,?) OR o.operation_name IN (?,?)) ORDER BY r.name,s.service_path,o.operation_name`,
34
34
  )
@@ -39,7 +39,12 @@ function rows(
39
39
  names.simplePath,
40
40
  names.name,
41
41
  names.simpleName,
42
- ) as unknown as OperationTarget[];
42
+ ) as Array<Omit<OperationTarget, 'reasons'>>;
43
+ return result.map((row) => ({
44
+ ...row,
45
+ score: Number(row.score ?? 0),
46
+ reasons: [],
47
+ }));
43
48
  }
44
49
  function operationLookupNames(operationPath: string): { path: string; simplePath: string; name: string; simpleName: string } {
45
50
  const name = operationPath.replace(/^\//, '');
@@ -70,7 +75,13 @@ export function resolveOperation(
70
75
  if (missing.length > 0)
71
76
  return {
72
77
  status: 'dynamic',
73
- candidates: signals.operationPath ? rows(db, signals.operationPath, workspaceId) : [],
78
+ candidates: signals.operationPath
79
+ ? rows(db, signals.operationPath, workspaceId).map((candidate) => ({
80
+ ...candidate,
81
+ score: 0.2,
82
+ reasons: ['operation_path_match'],
83
+ }))
84
+ : [],
74
85
  reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`),
75
86
  };
76
87
  if (!signals.operationPath)
@@ -28,8 +28,9 @@ function diagnosticLines(diagnostic: Record<string, unknown>): string[] {
28
28
  }
29
29
 
30
30
  function hintLines(evidence: Record<string, unknown>): string[] {
31
+ const dynamicLines = dynamicHintLines(evidence);
31
32
  const suggestions = evidence.implementationHintSuggestions;
32
- if (!Array.isArray(suggestions)) return [];
33
+ if (!Array.isArray(suggestions)) return dynamicLines;
33
34
  const hints = suggestions.flatMap((item) =>
34
35
  isRecord(item) && typeof item.cli === 'string'
35
36
  ? [item.cli]
@@ -38,9 +39,35 @@ function hintLines(evidence: Record<string, unknown>): string[] {
38
39
  const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
39
40
  if (unique.length > shown.length)
40
41
  shown.push(`... ${unique.length - shown.length} more hint(s) available in JSON`);
41
- return shown;
42
+ return [...dynamicLines, ...shown];
43
+ }
44
+
45
+ function dynamicHintLines(evidence: Record<string, unknown>): string[] {
46
+ const exploration = isRecord(evidence.dynamicTargetExploration)
47
+ ? evidence.dynamicTargetExploration
48
+ : evidence;
49
+ const count = numberValue(exploration.candidateCount);
50
+ if (count === 0) return [];
51
+ const shown = numberValue(exploration.shownCandidateCount);
52
+ const omitted = numberValue(exploration.omittedCandidateCount);
53
+ const lines = [`candidates: ${shown} shown, ${omitted} omitted`];
54
+ lines.push(...varSetHints(exploration.suggestedVarSets));
55
+ if (omitted > 0 || shown < count)
56
+ lines.push('use --dynamic-mode candidates --max-dynamic-candidates 20 to explore candidate branches');
57
+ return lines;
58
+ }
59
+
60
+ function varSetHints(value: unknown): string[] {
61
+ if (!Array.isArray(value)) return [];
62
+ const hints = value.flatMap((item) =>
63
+ isRecord(item) && typeof item.cli === 'string' ? [`try ${item.cli}`] : []);
64
+ return [...new Set(hints)].slice(0, 3);
42
65
  }
43
66
 
44
67
  function isRecord(value: unknown): value is Record<string, unknown> {
45
68
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
46
69
  }
70
+
71
+ function numberValue(value: unknown): number {
72
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
73
+ }
@@ -0,0 +1,45 @@
1
+ import type { TraceEdge } from '../types.js';
2
+
3
+ interface DynamicBranchCall {
4
+ repoName: string;
5
+ source_file: string;
6
+ source_line: number;
7
+ }
8
+
9
+ export function dynamicCandidateBranches(
10
+ depth: number,
11
+ call: DynamicBranchCall,
12
+ evidence: Record<string, unknown>,
13
+ ): TraceEdge[] {
14
+ const exploration = objectRecord(evidence.dynamicTargetExploration);
15
+ 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),
26
+ },
27
+ confidence: numericValue(candidate.score),
28
+ unresolvedReason: 'Exploratory dynamic target candidate; provide runtime variables to select it',
29
+ }));
30
+ }
31
+
32
+ function objectRecord(value: unknown): Record<string, unknown> {
33
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
34
+ }
35
+
36
+ function recordArray(value: unknown): Record<string, unknown>[] {
37
+ return Array.isArray(value)
38
+ ? value.filter((item): item is Record<string, unknown> =>
39
+ Boolean(item && typeof item === 'object' && !Array.isArray(item)))
40
+ : [];
41
+ }
42
+
43
+ function numericValue(value: unknown): number {
44
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
45
+ }