@saptools/service-flow 0.1.0

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.
@@ -0,0 +1,686 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/discovery/discover-repositories.ts
4
+ import fs from "fs/promises";
5
+ import path2 from "path";
6
+
7
+ // src/utils/path-utils.ts
8
+ import path from "path";
9
+ function normalizePath(value) {
10
+ return value.split(path.sep).join("/");
11
+ }
12
+ function relativePath(root, value) {
13
+ return normalizePath(path.relative(root, value) || ".");
14
+ }
15
+ function ensureLeadingSlash(value) {
16
+ return value.startsWith("/") ? value : `/${value}`;
17
+ }
18
+ function stripQuotes(value) {
19
+ return value.replace(/^['"`]|['"`]$/g, "");
20
+ }
21
+
22
+ // src/discovery/discover-repositories.ts
23
+ async function discoverRepositories(rootPath, ignore) {
24
+ const root = path2.resolve(rootPath);
25
+ const ignored = new Set(ignore);
26
+ const found = [];
27
+ async function walk(dir) {
28
+ const rel = relativePath(root, dir);
29
+ if (rel !== "." && rel.split("/").some((part) => ignored.has(part))) return;
30
+ let entries;
31
+ try {
32
+ entries = await fs.readdir(dir, { withFileTypes: true });
33
+ } catch {
34
+ return;
35
+ }
36
+ if (entries.some((e) => e.name === ".git" || e.name === ".git-fixture")) {
37
+ found.push({
38
+ name: path2.basename(dir),
39
+ absolutePath: dir,
40
+ relativePath: relativePath(root, dir),
41
+ isGitRepo: true
42
+ });
43
+ return;
44
+ }
45
+ for (const entry of entries)
46
+ if (entry.isDirectory() && !ignore.includes(entry.name))
47
+ await walk(path2.join(dir, entry.name));
48
+ }
49
+ await walk(root);
50
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
51
+ }
52
+
53
+ // src/parsers/package-json-parser.ts
54
+ import fs2 from "fs/promises";
55
+ import path3 from "path";
56
+ function recordOfString(value) {
57
+ if (!value || typeof value !== "object") return {};
58
+ return Object.fromEntries(
59
+ Object.entries(value).filter(([, v]) => typeof v === "string")
60
+ );
61
+ }
62
+ function readRequires(cds) {
63
+ const requires = cds && typeof cds === "object" && "requires" in cds ? cds.requires : void 0;
64
+ if (!requires || typeof requires !== "object") return [];
65
+ return Object.entries(requires).flatMap(([alias, raw]) => {
66
+ if (!raw || typeof raw !== "object") return [];
67
+ const obj = raw;
68
+ const credentials = obj.credentials && typeof obj.credentials === "object" ? obj.credentials : {};
69
+ return [
70
+ {
71
+ alias,
72
+ kind: typeof obj.kind === "string" ? obj.kind : void 0,
73
+ model: typeof obj.model === "string" ? obj.model : void 0,
74
+ destination: typeof credentials.destination === "string" ? credentials.destination : void 0,
75
+ servicePath: typeof credentials.path === "string" ? credentials.path : void 0,
76
+ requestTimeout: typeof credentials.requestTimeout === "number" ? credentials.requestTimeout : void 0,
77
+ rawJson: JSON.stringify(raw)
78
+ }
79
+ ];
80
+ });
81
+ }
82
+ async function parsePackageJson(repoPath) {
83
+ try {
84
+ const raw = await fs2.readFile(path3.join(repoPath, "package.json"), "utf8");
85
+ const json = JSON.parse(raw);
86
+ return {
87
+ packageName: typeof json.name === "string" ? json.name : void 0,
88
+ packageVersion: typeof json.version === "string" ? json.version : void 0,
89
+ dependencies: {
90
+ ...recordOfString(json.dependencies),
91
+ ...recordOfString(json.devDependencies)
92
+ },
93
+ cdsRequires: readRequires(json.cds),
94
+ scripts: recordOfString(json.scripts)
95
+ };
96
+ } catch {
97
+ return { dependencies: {}, cdsRequires: [], scripts: {} };
98
+ }
99
+ }
100
+
101
+ // src/parsers/cds-parser.ts
102
+ import fs3 from "fs/promises";
103
+ import path4 from "path";
104
+ function lineOf(text, index) {
105
+ return text.slice(0, index).split("\n").length;
106
+ }
107
+ function pathAnno(prefix) {
108
+ return /@\s*\(?\s*path\s*:\s*['"]([^'"]+)['"]\s*\)?/.exec(prefix)?.[1];
109
+ }
110
+ async function parseCdsFile(repoPath, filePath) {
111
+ const absolute = path4.join(repoPath, filePath);
112
+ const text = await fs3.readFile(absolute, "utf8");
113
+ const namespace = /namespace\s+([\w.]+)\s*;/.exec(text)?.[1];
114
+ const services = [];
115
+ const serviceRegex = /((?:@\s*\(?\s*path\s*:\s*['"][^'"]+['"]\s*\)?\s*)?)(extend\s+)?service\s+(\w+)\s*(?:@\s*\(?\s*path\s*:\s*['"]([^'"]+)['"]\s*\)?\s*)?\{/g;
116
+ let match;
117
+ while (match = serviceRegex.exec(text)) {
118
+ const serviceName = match[3] ?? "UnknownService";
119
+ const start = match.index + match[0].length;
120
+ let depth = 1;
121
+ let end = start;
122
+ for (; end < text.length; end += 1) {
123
+ const c = text[end];
124
+ if (c === "{") depth += 1;
125
+ if (c === "}") depth -= 1;
126
+ if (depth === 0) break;
127
+ }
128
+ const body = text.slice(start, end);
129
+ const servicePath = ensureLeadingSlash(
130
+ match[4] ?? pathAnno(match[1] ?? "") ?? serviceName
131
+ );
132
+ const ops = [
133
+ ...body.matchAll(
134
+ /\b(action|function|event)\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?:returns\s+([^;{]+))?/g
135
+ )
136
+ ].map((m) => ({
137
+ operationType: m[1] ?? "action",
138
+ operationName: m[2] ?? "unknown",
139
+ operationPath: ensureLeadingSlash(m[2] ?? "unknown"),
140
+ paramsJson: JSON.stringify(
141
+ (m[3] ?? "").split(",").map((p) => p.trim()).filter(Boolean)
142
+ ),
143
+ returnType: m[4]?.trim(),
144
+ sourceFile: normalizePath(filePath),
145
+ sourceLine: lineOf(text, start + (m.index ?? 0))
146
+ }));
147
+ services.push({
148
+ namespace,
149
+ serviceName,
150
+ qualifiedName: namespace ? `${namespace}.${serviceName}` : serviceName,
151
+ servicePath,
152
+ isExtend: Boolean(match[2]),
153
+ sourceFile: normalizePath(filePath),
154
+ sourceLine: lineOf(text, match.index),
155
+ operations: ops
156
+ });
157
+ serviceRegex.lastIndex = end;
158
+ }
159
+ return services;
160
+ }
161
+
162
+ // src/parsers/decorator-parser.ts
163
+ import fs4 from "fs/promises";
164
+ import path5 from "path";
165
+ import ts2 from "typescript";
166
+
167
+ // src/parsers/ts-project.ts
168
+ import ts from "typescript";
169
+ function createSourceFile(filePath, text) {
170
+ return ts.createSourceFile(
171
+ filePath,
172
+ text,
173
+ ts.ScriptTarget.Latest,
174
+ true,
175
+ filePath.endsWith(".js") ? ts.ScriptKind.JS : ts.ScriptKind.TS
176
+ );
177
+ }
178
+
179
+ // src/parsers/decorator-parser.ts
180
+ function line(sf, pos) {
181
+ return sf.getLineAndCharacterOfPosition(pos).line + 1;
182
+ }
183
+ function decs(node) {
184
+ return ts2.canHaveDecorators(node) ? [...ts2.getDecorators(node) ?? []] : [];
185
+ }
186
+ function callName(d) {
187
+ const e = d.expression;
188
+ return ts2.isCallExpression(e) ? e.expression.getText() : e.getText();
189
+ }
190
+ function firstArg(d) {
191
+ const e = d.expression;
192
+ return ts2.isCallExpression(e) && e.arguments[0] ? e.arguments[0].getText() : "";
193
+ }
194
+ async function parseDecorators(repoPath, filePath) {
195
+ const text = await fs4.readFile(path5.join(repoPath, filePath), "utf8");
196
+ const sf = createSourceFile(filePath, text);
197
+ const constants = /* @__PURE__ */ new Map();
198
+ const handlers = [];
199
+ function visit(node) {
200
+ if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.initializer && ts2.isStringLiteralLike(node.initializer))
201
+ constants.set(node.name.text, node.initializer.text);
202
+ if (ts2.isClassDeclaration(node)) {
203
+ const className = node.name?.text ?? "AnonymousHandler";
204
+ const hasHandler = decs(node).some((d) => callName(d) === "Handler");
205
+ const methods = node.members.filter(ts2.isMethodDeclaration).flatMap(
206
+ (m) => decs(m).filter(
207
+ (d) => ["Func", "Action", "On", "Event"].includes(callName(d))
208
+ ).map((d) => {
209
+ const raw = firstArg(d);
210
+ const value = raw.startsWith('"') || raw.startsWith("'") || raw.startsWith("`") ? stripQuotes(raw) : constants.get(raw) ?? (raw.endsWith(".name") ? raw.split(".").at(-2) : void 0);
211
+ return {
212
+ methodName: m.name.getText(),
213
+ decoratorKind: callName(d),
214
+ decoratorValue: value,
215
+ decoratorRawExpression: raw,
216
+ sourceFile: normalizePath(filePath),
217
+ sourceLine: line(sf, m.getStart())
218
+ };
219
+ })
220
+ );
221
+ if (hasHandler || methods.length > 0)
222
+ handlers.push({
223
+ className,
224
+ sourceFile: normalizePath(filePath),
225
+ sourceLine: line(sf, node.getStart()),
226
+ methods
227
+ });
228
+ }
229
+ ts2.forEachChild(node, visit);
230
+ }
231
+ visit(sf);
232
+ return handlers;
233
+ }
234
+
235
+ // src/parsers/handler-registration-parser.ts
236
+ import fs5 from "fs/promises";
237
+ import path6 from "path";
238
+ function lineOf2(text, idx) {
239
+ return text.slice(0, idx).split("\n").length;
240
+ }
241
+ async function parseHandlerRegistrations(repoPath, filePath) {
242
+ const text = await fs5.readFile(path6.join(repoPath, filePath), "utf8");
243
+ const out = [];
244
+ for (const m of text.matchAll(
245
+ /createCombinedHandler\s*\(|srv\.prepend\s*\(|cds\.serve\s*\(/g
246
+ ))
247
+ out.push({
248
+ registrationFile: normalizePath(filePath),
249
+ registrationLine: lineOf2(text, m.index ?? 0),
250
+ registrationKind: m[0].startsWith("cds") ? "cds.serve" : "combined-handler",
251
+ confidence: 0.8
252
+ });
253
+ for (const m of text.matchAll(
254
+ /(?:const|export\s+const)\s+handlers\s*=\s*\[([\s\S]*?)\]/g
255
+ ))
256
+ for (const c of (m[1] ?? "").matchAll(/\b(\w+Handler)\b/g))
257
+ out.push({
258
+ className: c[1],
259
+ registrationFile: normalizePath(filePath),
260
+ registrationLine: lineOf2(text, (m.index ?? 0) + (c.index ?? 0)),
261
+ registrationKind: "handler-array",
262
+ confidence: 0.9
263
+ });
264
+ return out;
265
+ }
266
+
267
+ // src/utils/redaction.ts
268
+ var SENSITIVE = /authorization|cookie|token|secret|password|key|credential/i;
269
+ function redactText(text) {
270
+ return text.replace(
271
+ /(authorization|cookie|token|secret|password|key|credential)\s*[:=]\s*(['"`]?)[^,'"`}\s]+\2/gi,
272
+ "$1: [REDACTED]"
273
+ );
274
+ }
275
+ function redactValue(value) {
276
+ if (Array.isArray(value)) return value.map(redactValue);
277
+ if (value && typeof value === "object") {
278
+ const out = {};
279
+ for (const [k, v] of Object.entries(value))
280
+ out[k] = SENSITIVE.test(k) ? "[REDACTED]" : redactValue(v);
281
+ return out;
282
+ }
283
+ return typeof value === "string" ? redactText(value) : value;
284
+ }
285
+ function summarizeExpression(text) {
286
+ return redactText(text).slice(0, 240);
287
+ }
288
+
289
+ // src/parsers/outbound-call-parser.ts
290
+ import fs6 from "fs/promises";
291
+ import path7 from "path";
292
+ function lineOf3(text, idx) {
293
+ return text.slice(0, idx).split("\n").length;
294
+ }
295
+ function firstArg2(body, key) {
296
+ return new RegExp(`${key}\\s*:\\s*([^,}\\n]+)`).exec(body)?.[1]?.trim();
297
+ }
298
+ async function parseOutboundCalls(repoPath, filePath) {
299
+ const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
300
+ const out = [];
301
+ for (const m of text.matchAll(/(\w+)\.send\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
302
+ const body = m[2] ?? "";
303
+ const query = firstArg2(body, "query");
304
+ const op = firstArg2(body, "path");
305
+ out.push({
306
+ callType: query ? "remote_query" : "remote_action",
307
+ serviceVariableName: m[1],
308
+ method: stripQuotes(firstArg2(body, "method") ?? "POST"),
309
+ operationPathExpr: op ? stripQuotes(op) : void 0,
310
+ queryEntity: /SELECT\.from\((\w+)\)/.exec(query ?? "")?.[1],
311
+ payloadSummary: summarizeExpression(body),
312
+ sourceFile: normalizePath(filePath),
313
+ sourceLine: lineOf3(text, m.index ?? 0),
314
+ confidence: op || query ? 0.8 : 0.4
315
+ });
316
+ }
317
+ for (const m of text.matchAll(/cds\.run\s*\(([\s\S]*?)\)/g))
318
+ out.push({
319
+ callType: "local_db_query",
320
+ queryEntity: /(?:SELECT\.from|INSERT\.into)\((\w+)\)/.exec(
321
+ m[1] ?? ""
322
+ )?.[1],
323
+ payloadSummary: summarizeExpression(m[1] ?? ""),
324
+ sourceFile: normalizePath(filePath),
325
+ sourceLine: lineOf3(text, m.index ?? 0),
326
+ confidence: 0.85
327
+ });
328
+ for (const m of text.matchAll(
329
+ /(\w+)\.(emit|publish|on)\s*\(\s*(['"`])([^'"`]+)\3/g
330
+ ))
331
+ out.push({
332
+ callType: m[2] === "on" ? "async_subscribe" : "async_emit",
333
+ serviceVariableName: m[1],
334
+ eventNameExpr: m[4],
335
+ sourceFile: normalizePath(filePath),
336
+ sourceLine: lineOf3(text, m.index ?? 0),
337
+ confidence: 0.8
338
+ });
339
+ for (const m of text.matchAll(
340
+ /(?:axios\s*\(|executeHttpRequest\s*\(|useOrFetchDestination\s*\()([\s\S]*?)\)/g
341
+ ))
342
+ out.push({
343
+ callType: "external_http",
344
+ payloadSummary: summarizeExpression(m[1] ?? ""),
345
+ sourceFile: normalizePath(filePath),
346
+ sourceLine: lineOf3(text, m.index ?? 0),
347
+ confidence: 0.7,
348
+ unresolvedReason: "External HTTP destination is outside indexed CAP services"
349
+ });
350
+ for (const m of text.matchAll(
351
+ /cds\.services(?:\[['"]([^'"]+)['"]\]|\.(\w+))\.(\w+)\s*\(/g
352
+ ))
353
+ out.push({
354
+ callType: "local_service_call",
355
+ operationPathExpr: `/${m[3] ?? ""}`,
356
+ payloadSummary: m[1] ?? m[2],
357
+ sourceFile: normalizePath(filePath),
358
+ sourceLine: lineOf3(text, m.index ?? 0),
359
+ confidence: 0.75
360
+ });
361
+ return out;
362
+ }
363
+
364
+ // src/parsers/service-binding-parser.ts
365
+ import fs7 from "fs/promises";
366
+ import path8 from "path";
367
+ function lineOf4(text, idx) {
368
+ return text.slice(0, idx).split("\n").length;
369
+ }
370
+ function placeholders(value) {
371
+ return [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((m) => m[1] ?? "").filter(Boolean);
372
+ }
373
+ async function parseServiceBindings(repoPath, filePath) {
374
+ const text = await fs7.readFile(path8.join(repoPath, filePath), "utf8");
375
+ const out = [];
376
+ for (const m of text.matchAll(
377
+ /(?:const|let|this\.)\s*(\w+)\s*=\s*(?:await\s*)?cds\.connect\.to\((['"`])([^'"`]+)\2\)/g
378
+ ))
379
+ out.push({
380
+ variableName: m[1] ?? "service",
381
+ alias: m[3],
382
+ isDynamic: false,
383
+ placeholders: [],
384
+ sourceFile: normalizePath(filePath),
385
+ sourceLine: lineOf4(text, m.index ?? 0)
386
+ });
387
+ for (const m of text.matchAll(
388
+ /(?:const|let|this\.)\s*(\w+)\s*=\s*(?:await\s*)?cds\.connect\.to\(\{([\s\S]*?)\}\s*\)/g
389
+ )) {
390
+ const body = m[2] ?? "";
391
+ const destination = /destination\s*:\s*([^,\n]+)/.exec(body)?.[1];
392
+ const servicePath = /path\s*:\s*([^,\n]+)/.exec(body)?.[1];
393
+ const dest = destination ? stripQuotes(destination.trim()) : void 0;
394
+ const svc = servicePath ? stripQuotes(servicePath.trim()) : void 0;
395
+ const ph = [...placeholders(dest), ...placeholders(svc)];
396
+ out.push({
397
+ variableName: m[1] ?? "service",
398
+ destinationExpr: dest,
399
+ servicePathExpr: svc,
400
+ isDynamic: ph.length > 0,
401
+ placeholders: ph,
402
+ sourceFile: normalizePath(filePath),
403
+ sourceLine: lineOf4(text, m.index ?? 0)
404
+ });
405
+ }
406
+ for (const m of text.matchAll(
407
+ /function\s+(\w+)\s*\([^)]*\)\s*\{[\s\S]*?return\s+cds\.connect\.to\((['"])([^'"]+)\2\)/g
408
+ ))
409
+ out.push({
410
+ variableName: m[1] ?? "connect",
411
+ alias: m[3],
412
+ isDynamic: false,
413
+ placeholders: [],
414
+ sourceFile: normalizePath(filePath),
415
+ sourceLine: lineOf4(text, m.index ?? 0)
416
+ });
417
+ return out;
418
+ }
419
+
420
+ // src/linker/dynamic-edge-resolver.ts
421
+ function applyVariables(template, vars) {
422
+ if (!template) return void 0;
423
+ return template.replace(
424
+ /\$\{\s*(\w+)\s*\}/g,
425
+ (_m, key) => vars[key] ?? `\${${key}}`
426
+ );
427
+ }
428
+
429
+ // src/linker/service-resolver.ts
430
+ function findOperation(db, servicePath, operationPath, workspaceId) {
431
+ if (!operationPath) return void 0;
432
+ const row = db.prepare(
433
+ `SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine
434
+ FROM cds_operations o
435
+ JOIN cds_services s ON s.id=o.service_id
436
+ JOIN repositories r ON r.id=s.repo_id
437
+ WHERE (? IS NULL OR r.workspace_id=?)
438
+ AND (? IS NULL OR s.service_path=?)
439
+ AND (o.operation_path=? OR o.operation_name=?)
440
+ ORDER BY CASE WHEN s.service_path=? THEN 0 ELSE 1 END
441
+ LIMIT 1`
442
+ ).get(
443
+ workspaceId,
444
+ workspaceId,
445
+ servicePath,
446
+ servicePath,
447
+ operationPath,
448
+ operationPath.replace(/^\//, ""),
449
+ servicePath
450
+ );
451
+ return row;
452
+ }
453
+
454
+ // src/linker/helper-package-linker.ts
455
+ function linkHelperPackages(db, workspaceId) {
456
+ const repos = db.prepare(
457
+ "SELECT id,name,dependencies_json FROM repositories WHERE workspace_id=?"
458
+ ).all(workspaceId);
459
+ let count = 0;
460
+ for (const repo of repos) {
461
+ const deps = JSON.parse(repo.dependencies_json);
462
+ for (const dep of Object.keys(deps)) {
463
+ const helper = repos.find((r) => r.name === dep || dep.endsWith(r.name));
464
+ if (helper) {
465
+ db.prepare(
466
+ "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?)"
467
+ ).run(
468
+ workspaceId,
469
+ "REPO_IMPORTS_HELPER_PACKAGE",
470
+ "repo",
471
+ String(repo.id),
472
+ "repo",
473
+ String(helper.id),
474
+ 0.9,
475
+ JSON.stringify({ dependency: dep }),
476
+ 0
477
+ );
478
+ count += 1;
479
+ }
480
+ }
481
+ }
482
+ return count;
483
+ }
484
+
485
+ // src/linker/cross-repo-linker.ts
486
+ function linkWorkspace(db, workspaceId, vars = {}) {
487
+ db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
488
+ let edges = linkHelperPackages(db, workspaceId);
489
+ let unresolved = 0;
490
+ const calls = db.prepare(
491
+ `SELECT c.*,r.name repoName,b.alias,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE r.workspace_id=?`
492
+ ).all(workspaceId);
493
+ for (const call of calls) {
494
+ const callType = String(call.call_type);
495
+ const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
496
+ const servicePath = applyVariables(
497
+ call.servicePathExpr ?? call.requireServicePath,
498
+ vars
499
+ );
500
+ const target = callType.startsWith("remote") ? findOperation(db, servicePath, op, workspaceId) : void 0;
501
+ const evidence = {
502
+ sourceFile: call.source_file,
503
+ sourceLine: call.source_line,
504
+ serviceAlias: call.alias,
505
+ destination: call.destinationExpr ?? call.requireDestination,
506
+ servicePath,
507
+ operationPath: op,
508
+ targetRepo: target?.repoName,
509
+ targetOperation: target?.operationName
510
+ };
511
+ if (target) {
512
+ db.prepare(
513
+ "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?)"
514
+ ).run(
515
+ workspaceId,
516
+ "REMOTE_CALL_RESOLVES_TO_OPERATION",
517
+ "call",
518
+ String(call.id),
519
+ "operation",
520
+ String(target.operationId),
521
+ call.isDynamic ? 0.6 : 0.9,
522
+ JSON.stringify(evidence),
523
+ call.isDynamic ? 1 : 0
524
+ );
525
+ edges += 1;
526
+ } else {
527
+ const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : call.isDynamic ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
528
+ db.prepare(
529
+ "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?)"
530
+ ).run(
531
+ workspaceId,
532
+ edgeType,
533
+ "call",
534
+ String(call.id),
535
+ callType.startsWith("async_") ? "event" : "external",
536
+ String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
537
+ Number(call.confidence ?? 0.2),
538
+ JSON.stringify(evidence),
539
+ call.isDynamic ? 1 : 0,
540
+ String(call.unresolved_reason ?? "No indexed target operation matched")
541
+ );
542
+ edges += 1;
543
+ unresolved += edgeType === "UNRESOLVED_EDGE" ? 1 : 0;
544
+ }
545
+ }
546
+ return { edgeCount: edges, unresolvedCount: unresolved };
547
+ }
548
+
549
+ // src/trace/traversal.ts
550
+ function limitDepth(edges, depth) {
551
+ return edges.filter((edge) => edge.step <= depth);
552
+ }
553
+
554
+ // src/trace/trace-engine.ts
555
+ function normalizeOperation(value) {
556
+ if (!value) return void 0;
557
+ return value.startsWith("/") ? value.slice(1) : value;
558
+ }
559
+ function positiveDepth(value) {
560
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
561
+ }
562
+ function sourceFilesForStart(db, repoId, start) {
563
+ const handler = start.handler;
564
+ const operation = normalizeOperation(start.operation ?? start.operationPath);
565
+ if (!handler && !operation) return void 0;
566
+ const rows = db.prepare(
567
+ `SELECT DISTINCT hc.source_file sourceFile
568
+ FROM handler_classes hc
569
+ LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
570
+ WHERE (? IS NULL OR hc.repo_id=?)
571
+ AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
572
+ AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)`
573
+ ).all(
574
+ repoId,
575
+ repoId,
576
+ handler,
577
+ handler,
578
+ handler,
579
+ operation,
580
+ operation,
581
+ operation
582
+ );
583
+ if (rows.length === 0) return void 0;
584
+ return new Set(
585
+ rows.map((row) => row.sourceFile).filter(Boolean)
586
+ );
587
+ }
588
+ function startScope(db, start) {
589
+ const repo = start.repo ? db.prepare(
590
+ "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
591
+ ).get(start.repo, start.repo) : void 0;
592
+ if (start.repo && !repo)
593
+ return {
594
+ repo,
595
+ selectorMatched: false
596
+ };
597
+ const sourceFiles = sourceFilesForStart(db, repo?.id, start);
598
+ const hasSelector = Boolean(
599
+ start.handler ?? start.operation ?? start.operationPath
600
+ );
601
+ return {
602
+ repo,
603
+ sourceFiles,
604
+ selectorMatched: !hasSelector || sourceFiles !== void 0
605
+ };
606
+ }
607
+ function trace(db, start, options) {
608
+ const scope = startScope(db, start);
609
+ const calls = db.prepare(
610
+ `SELECT c.*,r.name repoName,b.alias,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`
611
+ ).all(scope.repo?.id, scope.repo?.id);
612
+ const vars = options.vars ?? {};
613
+ const filtered = calls.filter((c) => {
614
+ if (!scope.selectorMatched) return false;
615
+ if (scope.sourceFiles && !scope.sourceFiles.has(String(c.source_file)))
616
+ return false;
617
+ const type = String(c.call_type);
618
+ if (!options.includeDb && type === "local_db_query") return false;
619
+ if (!options.includeExternal && type === "external_http") return false;
620
+ if (!options.includeAsync && type.startsWith("async_")) return false;
621
+ return true;
622
+ });
623
+ const edges = filtered.map((c, index) => {
624
+ const operation = applyVariables(
625
+ c.operation_path_expr,
626
+ vars
627
+ );
628
+ const servicePath = applyVariables(
629
+ c.servicePathExpr ?? c.requireServicePath,
630
+ vars
631
+ );
632
+ const type = String(c.call_type);
633
+ const to = type === "local_db_query" ? `Entity: ${String(c.query_entity ?? "unknown")}` : type === "async_emit" ? `Topic: ${String(c.event_name_expr ?? "unknown")}` : type === "async_subscribe" ? `Topic: ${String(c.event_name_expr ?? "unknown")}` : type === "external_http" ? "External HTTP destination" : `${servicePath ?? ""}${operation ?? ""}` || String(c.event_name_expr ?? "unknown");
634
+ return {
635
+ step: index + 1,
636
+ type: c.isDynamic ? "dynamic_action" : type,
637
+ from: `${String(c.repoName)}:${String(c.source_file)}`,
638
+ to,
639
+ evidence: {
640
+ file: c.source_file,
641
+ line: c.source_line,
642
+ alias: c.alias,
643
+ destination: c.destinationExpr ?? c.requireDestination,
644
+ servicePath,
645
+ operationPath: operation,
646
+ method: c.method,
647
+ payloadSummary: c.payload_summary
648
+ },
649
+ confidence: Number(c.confidence ?? 0.5),
650
+ unresolvedReason: c.unresolved_reason
651
+ };
652
+ });
653
+ const diagnostics = db.prepare(
654
+ "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
655
+ ).all(scope.repo?.id, scope.repo?.id);
656
+ if (!scope.selectorMatched)
657
+ diagnostics.unshift({
658
+ severity: "warning",
659
+ code: "trace_start_not_found",
660
+ message: "No handler source matched the requested trace start selector"
661
+ });
662
+ return {
663
+ start,
664
+ nodes: [],
665
+ edges: limitDepth(edges, positiveDepth(options.depth)),
666
+ diagnostics
667
+ };
668
+ }
669
+
670
+ export {
671
+ normalizePath,
672
+ stripQuotes,
673
+ discoverRepositories,
674
+ parsePackageJson,
675
+ parseCdsFile,
676
+ parseDecorators,
677
+ parseHandlerRegistrations,
678
+ redactText,
679
+ redactValue,
680
+ parseOutboundCalls,
681
+ parseServiceBindings,
682
+ applyVariables,
683
+ linkWorkspace,
684
+ trace
685
+ };
686
+ //# sourceMappingURL=chunk-RFBHT6BQ.js.map