@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/CHANGELOG.md +7 -0
- package/README.md +9 -1
- package/TECHNICAL-NOTE.md +6 -1
- package/dist/{chunk-52OUS3MO.js → chunk-YZJKE5UX.js} +470 -29
- package/dist/chunk-YZJKE5UX.js.map +1 -0
- package/dist/cli.js +42 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -10
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +22 -0
- package/src/index.ts +1 -1
- package/src/linker/cross-repo-linker.ts +9 -1
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +29 -2
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +424 -0
- package/src/trace/evidence.ts +117 -7
- package/src/trace/trace-engine.ts +11 -11
- package/src/types.ts +12 -0
- package/dist/chunk-52OUS3MO.js.map +0 -1
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
package/package.json
CHANGED
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 [{
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|