@saptools/service-flow 0.1.2 → 0.1.4

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,1313 @@
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 isRealGitMarker(dir) {
28
+ const gitPath = path2.join(dir, ".git");
29
+ try {
30
+ const st = await fs.stat(gitPath);
31
+ if (st.isDirectory()) {
32
+ const children = await fs.readdir(gitPath);
33
+ return children.includes("HEAD") || children.includes("config");
34
+ }
35
+ if (st.isFile()) {
36
+ const text = await fs.readFile(gitPath, "utf8");
37
+ return text.trimStart().startsWith("gitdir:");
38
+ }
39
+ } catch {
40
+ }
41
+ try {
42
+ const fixture = await fs.stat(path2.join(dir, ".git-fixture"));
43
+ return fixture.isFile() || fixture.isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ async function walk(dir) {
49
+ const rel = relativePath(root, dir);
50
+ if (rel !== "." && rel.split("/").some((part) => ignored.has(part))) return;
51
+ let entries;
52
+ try {
53
+ entries = await fs.readdir(dir, { withFileTypes: true });
54
+ } catch {
55
+ return;
56
+ }
57
+ const hasMarker = entries.some((e) => e.name === ".git" || e.name === ".git-fixture");
58
+ if (hasMarker && await isRealGitMarker(dir)) {
59
+ found.push({
60
+ name: path2.basename(dir),
61
+ absolutePath: dir,
62
+ relativePath: relativePath(root, dir),
63
+ isGitRepo: true
64
+ });
65
+ }
66
+ for (const entry of entries)
67
+ if (entry.isDirectory() && !ignore.includes(entry.name))
68
+ await walk(path2.join(dir, entry.name));
69
+ }
70
+ await walk(root);
71
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
72
+ }
73
+
74
+ // src/parsers/package-json-parser.ts
75
+ import fs2 from "fs/promises";
76
+ import path3 from "path";
77
+ function recordOfString(value) {
78
+ if (!value || typeof value !== "object") return {};
79
+ return Object.fromEntries(
80
+ Object.entries(value).filter(([, v]) => typeof v === "string")
81
+ );
82
+ }
83
+ function readRequires(cds) {
84
+ const requires = cds && typeof cds === "object" && "requires" in cds ? cds.requires : void 0;
85
+ if (!requires || typeof requires !== "object") return [];
86
+ return Object.entries(requires).flatMap(([alias, raw]) => {
87
+ if (!raw || typeof raw !== "object") return [];
88
+ const obj = raw;
89
+ const credentials = obj.credentials && typeof obj.credentials === "object" ? obj.credentials : {};
90
+ return [
91
+ {
92
+ alias,
93
+ kind: typeof obj.kind === "string" ? obj.kind : void 0,
94
+ model: typeof obj.model === "string" ? obj.model : void 0,
95
+ destination: typeof credentials.destination === "string" ? credentials.destination : void 0,
96
+ servicePath: typeof credentials.path === "string" ? credentials.path : void 0,
97
+ requestTimeout: typeof credentials.requestTimeout === "number" ? credentials.requestTimeout : void 0,
98
+ rawJson: JSON.stringify(raw)
99
+ }
100
+ ];
101
+ });
102
+ }
103
+ async function parsePackageJson(repoPath) {
104
+ try {
105
+ const raw = await fs2.readFile(path3.join(repoPath, "package.json"), "utf8");
106
+ const json = JSON.parse(raw);
107
+ return {
108
+ packageName: typeof json.name === "string" ? json.name : void 0,
109
+ packageVersion: typeof json.version === "string" ? json.version : void 0,
110
+ dependencies: {
111
+ ...recordOfString(json.dependencies),
112
+ ...recordOfString(json.devDependencies)
113
+ },
114
+ cdsRequires: readRequires(json.cds),
115
+ scripts: recordOfString(json.scripts)
116
+ };
117
+ } catch {
118
+ return { dependencies: {}, cdsRequires: [], scripts: {} };
119
+ }
120
+ }
121
+
122
+ // src/parsers/cds-parser.ts
123
+ import fs3 from "fs/promises";
124
+ import path4 from "path";
125
+ function lineOf(text, index) {
126
+ return text.slice(0, index).split("\n").length;
127
+ }
128
+ function maskCommentsAndStrings(text) {
129
+ let out = "";
130
+ let mode = "code";
131
+ for (let i = 0; i < text.length; i += 1) {
132
+ const c = text[i] ?? "";
133
+ const n = text[i + 1] ?? "";
134
+ if (mode === "code" && c === "/" && n === "/") {
135
+ mode = "line";
136
+ out += " ";
137
+ i += 1;
138
+ continue;
139
+ }
140
+ if (mode === "code" && c === "/" && n === "*") {
141
+ mode = "block";
142
+ out += " ";
143
+ i += 1;
144
+ continue;
145
+ }
146
+ if (mode === "line" && c === "\n") mode = "code";
147
+ if (mode === "block" && c === "*" && n === "/") {
148
+ mode = "code";
149
+ out += " ";
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (mode === "code" && (c === "'" || c === '"' || c === "`")) {
154
+ mode = c === "'" ? "single" : c === '"' ? "double" : "template";
155
+ out += " ";
156
+ continue;
157
+ }
158
+ if (mode === "single" && c === "'" || mode === "double" && c === '"' || mode === "template" && c === "`")
159
+ mode = "code";
160
+ out += mode === "code" || c === "\n" ? c : " ";
161
+ }
162
+ return out;
163
+ }
164
+ function readAnnotation(text, index) {
165
+ if (text[index] !== "@") return void 0;
166
+ let i = index + 1;
167
+ while (/\s/.test(text[i] ?? "")) i += 1;
168
+ if (text[i] !== "(") return void 0;
169
+ let depth = 0;
170
+ for (; i < text.length; i += 1) {
171
+ if (text[i] === "(") depth += 1;
172
+ if (text[i] === ")") depth -= 1;
173
+ if (depth === 0) return { end: i + 1, raw: text.slice(index, i + 1) };
174
+ }
175
+ return void 0;
176
+ }
177
+ function collectAnnotations(text, index) {
178
+ let i = index;
179
+ let raw = "";
180
+ while (i < text.length) {
181
+ while (/\s/.test(text[i] ?? "")) i += 1;
182
+ const annotation = readAnnotation(text, i);
183
+ if (!annotation) break;
184
+ raw += annotation.raw;
185
+ i = annotation.end;
186
+ }
187
+ return { end: i, raw };
188
+ }
189
+ function pathAnnotation(raw) {
190
+ return /path\s*:\s*['"]([^'"]+)['"]/s.exec(raw)?.[1];
191
+ }
192
+ function matchingBrace(text, open) {
193
+ let depth = 0;
194
+ for (let i = open; i < text.length; i += 1) {
195
+ if (text[i] === "{") depth += 1;
196
+ if (text[i] === "}") depth -= 1;
197
+ if (depth === 0) return i;
198
+ }
199
+ return text.length - 1;
200
+ }
201
+ function operationsFromBody(text, maskedBody, bodyOffset, filePath) {
202
+ return [...maskedBody.matchAll(/\b(action|function|event)\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?:returns\s+([^;{]+))?/g)].map((m) => ({
203
+ operationType: m[1] ?? "action",
204
+ operationName: m[2] ?? "unknown",
205
+ operationPath: ensureLeadingSlash(m[2] ?? "unknown"),
206
+ paramsJson: JSON.stringify((m[3] ?? "").split(",").map((part) => part.trim()).filter(Boolean)),
207
+ returnType: m[4]?.trim(),
208
+ sourceFile: normalizePath(filePath),
209
+ sourceLine: lineOf(text, bodyOffset + (m.index ?? 0))
210
+ }));
211
+ }
212
+ async function parseCdsFile(repoPath, filePath) {
213
+ const absolute = path4.join(repoPath, filePath);
214
+ const text = await fs3.readFile(absolute, "utf8");
215
+ const masked = maskCommentsAndStrings(text);
216
+ const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
217
+ const services = [];
218
+ const pendingAnnotations = [];
219
+ for (const a of masked.matchAll(/@\s*\(/g)) {
220
+ const annotation = collectAnnotations(masked, a.index ?? 0);
221
+ pendingAnnotations.push(annotation);
222
+ }
223
+ const serviceRegex = /\b(extend\s+)?service\s+([\w.]+)\b/g;
224
+ let match;
225
+ while (match = serviceRegex.exec(masked)) {
226
+ const afterName = collectAnnotations(masked, serviceRegex.lastIndex);
227
+ const open = masked.indexOf("{", afterName.end);
228
+ if (open === -1) continue;
229
+ const matchIndex = match.index;
230
+ const prefix = pendingAnnotations.filter((a) => a.end <= matchIndex && matchIndex - a.end < 8).map((a) => a.raw).join("");
231
+ const annotations = `${prefix}${afterName.raw}`;
232
+ const end = matchingBrace(masked, open);
233
+ const body = masked.slice(open + 1, end);
234
+ const name = match[2] ?? "UnknownService";
235
+ const serviceName = name.split(".").pop() ?? name;
236
+ const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
237
+ services.push({
238
+ namespace,
239
+ serviceName,
240
+ qualifiedName: name.includes(".") ? name : namespace ? `${namespace}.${name}` : name,
241
+ servicePath,
242
+ isExtend: Boolean(match[1]),
243
+ sourceFile: normalizePath(filePath),
244
+ sourceLine: lineOf(text, match.index),
245
+ operations: operationsFromBody(text, body, open + 1, filePath)
246
+ });
247
+ serviceRegex.lastIndex = end + 1;
248
+ }
249
+ const baseOps = new Map(services.filter((s) => !s.isExtend).map((s) => [s.qualifiedName, s.operations]));
250
+ for (const service of services.filter((s) => s.isExtend && s.operations.length === 0)) {
251
+ const inherited = baseOps.get(service.qualifiedName) ?? baseOps.get(service.serviceName);
252
+ if (inherited) service.operations = inherited.map((op) => ({ ...op, sourceFile: service.sourceFile, sourceLine: service.sourceLine }));
253
+ }
254
+ return services;
255
+ }
256
+
257
+ // src/parsers/decorator-parser.ts
258
+ import fs4 from "fs/promises";
259
+ import path5 from "path";
260
+ import ts2 from "typescript";
261
+
262
+ // src/parsers/ts-project.ts
263
+ import ts from "typescript";
264
+ function createSourceFile(filePath, text) {
265
+ return ts.createSourceFile(
266
+ filePath,
267
+ text,
268
+ ts.ScriptTarget.Latest,
269
+ true,
270
+ filePath.endsWith(".js") ? ts.ScriptKind.JS : ts.ScriptKind.TS
271
+ );
272
+ }
273
+
274
+ // src/parsers/decorator-parser.ts
275
+ function line(sf, pos) {
276
+ return sf.getLineAndCharacterOfPosition(pos).line + 1;
277
+ }
278
+ function decs(node) {
279
+ return ts2.canHaveDecorators(node) ? [...ts2.getDecorators(node) ?? []] : [];
280
+ }
281
+ function callName(d) {
282
+ const e = d.expression;
283
+ return ts2.isCallExpression(e) ? e.expression.getText() : e.getText();
284
+ }
285
+ function firstArg(d) {
286
+ const e = d.expression;
287
+ return ts2.isCallExpression(e) && e.arguments[0] ? e.arguments[0].getText() : "";
288
+ }
289
+ async function parseDecorators(repoPath, filePath) {
290
+ const text = await fs4.readFile(path5.join(repoPath, filePath), "utf8");
291
+ const sf = createSourceFile(filePath, text);
292
+ const constants = /* @__PURE__ */ new Map();
293
+ const handlers = [];
294
+ function visit(node) {
295
+ if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.initializer && ts2.isStringLiteralLike(node.initializer))
296
+ constants.set(node.name.text, node.initializer.text);
297
+ if (ts2.isClassDeclaration(node)) {
298
+ const className = node.name?.text ?? "AnonymousHandler";
299
+ const hasHandler = decs(node).some((d) => callName(d) === "Handler");
300
+ const methods = node.members.filter(ts2.isMethodDeclaration).flatMap(
301
+ (m) => decs(m).filter(
302
+ (d) => ["Func", "Action", "On", "Event"].includes(callName(d))
303
+ ).map((d) => {
304
+ const raw = firstArg(d);
305
+ const value = raw.startsWith('"') || raw.startsWith("'") || raw.startsWith("`") ? stripQuotes(raw) : constants.get(raw) ?? (raw.endsWith(".name") ? raw.split(".").at(-2) : void 0);
306
+ return {
307
+ methodName: m.name.getText(),
308
+ decoratorKind: callName(d),
309
+ decoratorValue: value,
310
+ decoratorRawExpression: raw,
311
+ sourceFile: normalizePath(filePath),
312
+ sourceLine: line(sf, m.getStart())
313
+ };
314
+ })
315
+ );
316
+ if (hasHandler || methods.length > 0)
317
+ handlers.push({
318
+ className,
319
+ sourceFile: normalizePath(filePath),
320
+ sourceLine: line(sf, node.getStart()),
321
+ methods
322
+ });
323
+ }
324
+ ts2.forEachChild(node, visit);
325
+ }
326
+ visit(sf);
327
+ return handlers;
328
+ }
329
+
330
+ // src/parsers/handler-registration-parser.ts
331
+ import fs5 from "fs/promises";
332
+ import path6 from "path";
333
+ function lineOf2(text, idx) {
334
+ return text.slice(0, idx).split("\n").length;
335
+ }
336
+ async function parseHandlerRegistrations(repoPath, filePath) {
337
+ const text = await fs5.readFile(path6.join(repoPath, filePath), "utf8");
338
+ const out = [];
339
+ for (const m of text.matchAll(
340
+ /createCombinedHandler\s*\(|srv\.prepend\s*\(|cds\.serve\s*\(/g
341
+ ))
342
+ out.push({
343
+ registrationFile: normalizePath(filePath),
344
+ registrationLine: lineOf2(text, m.index ?? 0),
345
+ registrationKind: m[0].startsWith("cds") ? "cds.serve" : "combined-handler",
346
+ confidence: 0.8
347
+ });
348
+ for (const m of text.matchAll(
349
+ /(?:const|export\s+const)\s+handlers\s*=\s*\[([\s\S]*?)\]/g
350
+ ))
351
+ for (const c of (m[1] ?? "").matchAll(/\b(\w+Handler)\b/g))
352
+ out.push({
353
+ className: c[1],
354
+ registrationFile: normalizePath(filePath),
355
+ registrationLine: lineOf2(text, (m.index ?? 0) + (c.index ?? 0)),
356
+ registrationKind: "handler-array",
357
+ confidence: 0.9
358
+ });
359
+ return out;
360
+ }
361
+
362
+ // src/utils/redaction.ts
363
+ var SENSITIVE = /authorization|cookie|token|secret|password|key|credential/i;
364
+ function redactText(text) {
365
+ return text.replace(
366
+ /(authorization|cookie|token|secret|password|key|credential)\s*[:=]\s*(['"`]?)[^,'"`}\s]+\2/gi,
367
+ "$1: [REDACTED]"
368
+ );
369
+ }
370
+ function redactValue(value) {
371
+ if (Array.isArray(value)) return value.map(redactValue);
372
+ if (value && typeof value === "object") {
373
+ const out = {};
374
+ for (const [k, v] of Object.entries(value))
375
+ out[k] = SENSITIVE.test(k) ? "[REDACTED]" : redactValue(v);
376
+ return out;
377
+ }
378
+ return typeof value === "string" ? redactText(value) : value;
379
+ }
380
+ function summarizeExpression(text) {
381
+ return redactText(text).slice(0, 240);
382
+ }
383
+
384
+ // src/parsers/outbound-call-parser.ts
385
+ import fs6 from "fs/promises";
386
+ import path7 from "path";
387
+ function lineOf3(text, idx) {
388
+ return text.slice(0, idx).split("\n").length;
389
+ }
390
+ function firstArg2(body, key) {
391
+ return new RegExp(`${key}\\s*:\\s*([^,}\\n]+)`).exec(body)?.[1]?.trim();
392
+ }
393
+ function matchingParen(text, open) {
394
+ let depth = 0;
395
+ let quote;
396
+ let escaped = false;
397
+ for (let i = open; i < text.length; i += 1) {
398
+ const ch = text[i] ?? "";
399
+ if (quote) {
400
+ if (escaped) escaped = false;
401
+ else if (ch === "\\") escaped = true;
402
+ else if (ch === quote) quote = void 0;
403
+ continue;
404
+ }
405
+ if (ch === '"' || ch === "'" || ch === "`") {
406
+ quote = ch;
407
+ continue;
408
+ }
409
+ if (ch === "(") depth += 1;
410
+ if (ch === ")") {
411
+ depth -= 1;
412
+ if (depth === 0) return i;
413
+ }
414
+ }
415
+ return -1;
416
+ }
417
+ function argumentForCall(expr, marker) {
418
+ const idx = expr.indexOf(marker);
419
+ if (idx < 0) return void 0;
420
+ const open = expr.indexOf("(", idx + marker.length);
421
+ if (open < 0) return void 0;
422
+ const close = matchingParen(expr, open);
423
+ return close > open ? expr.slice(open + 1, close).trim() : void 0;
424
+ }
425
+ function entityFromArg(arg) {
426
+ if (!arg) return void 0;
427
+ const first = arg.split(",")[0]?.trim();
428
+ if (!first) return void 0;
429
+ return stripQuotes(first).replace(/^this\./, "");
430
+ }
431
+ function extractQueryEntity(expr) {
432
+ return entityFromArg(argumentForCall(expr, "SELECT.one.from")) ?? entityFromArg(argumentForCall(expr, "SELECT.from")) ?? entityFromArg(argumentForCall(expr, "INSERT.into")) ?? entityFromArg(argumentForCall(expr, "UPDATE")) ?? entityFromArg(argumentForCall(expr, "DELETE.from"));
433
+ }
434
+ async function parseOutboundCalls(repoPath, filePath) {
435
+ const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
436
+ const out = [];
437
+ for (const m of text.matchAll(/(\w+)\.send\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
438
+ const body = m[2] ?? "";
439
+ const query = firstArg2(body, "query");
440
+ const op = firstArg2(body, "path") ?? firstArg2(body, "event");
441
+ out.push({
442
+ callType: query ? "remote_query" : "remote_action",
443
+ serviceVariableName: m[1],
444
+ method: stripQuotes(firstArg2(body, "method") ?? "POST"),
445
+ operationPathExpr: op ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0,
446
+ queryEntity: extractQueryEntity(query ?? ""),
447
+ payloadSummary: summarizeExpression(body),
448
+ sourceFile: normalizePath(filePath),
449
+ sourceLine: lineOf3(text, m.index ?? 0),
450
+ confidence: op || query ? 0.8 : 0.4
451
+ });
452
+ }
453
+ for (const m of text.matchAll(/cds\.run\s*\(/g)) {
454
+ const open = (m.index ?? 0) + m[0].lastIndexOf("(");
455
+ const close = matchingParen(text, open);
456
+ const expr = close > open ? text.slice(open + 1, close) : "";
457
+ const entity = extractQueryEntity(expr);
458
+ out.push({
459
+ callType: "local_db_query",
460
+ queryEntity: entity,
461
+ payloadSummary: summarizeExpression(expr),
462
+ sourceFile: normalizePath(filePath),
463
+ sourceLine: lineOf3(text, m.index ?? 0),
464
+ confidence: entity ? 0.9 : 0.55,
465
+ unresolvedReason: entity ? void 0 : "Could not resolve CAP query target entity from nested expression"
466
+ });
467
+ }
468
+ for (const m of text.matchAll(
469
+ /(\w+)\.(emit|publish|on)\s*\(\s*(['"`])([^'"`]+)\3/g
470
+ ))
471
+ out.push({
472
+ callType: m[2] === "on" ? "async_subscribe" : "async_emit",
473
+ serviceVariableName: m[1],
474
+ eventNameExpr: m[4],
475
+ sourceFile: normalizePath(filePath),
476
+ sourceLine: lineOf3(text, m.index ?? 0),
477
+ confidence: 0.8
478
+ });
479
+ for (const m of text.matchAll(
480
+ /(?:axios\s*\(|executeHttpRequest\s*\(|useOrFetchDestination\s*\()([\s\S]*?)\)/g
481
+ ))
482
+ out.push({
483
+ callType: "external_http",
484
+ payloadSummary: summarizeExpression(m[1] ?? ""),
485
+ sourceFile: normalizePath(filePath),
486
+ sourceLine: lineOf3(text, m.index ?? 0),
487
+ confidence: 0.7,
488
+ unresolvedReason: "External HTTP destination is outside indexed CAP services"
489
+ });
490
+ for (const m of text.matchAll(
491
+ /cds\.services(?:\[['"]([^'"]+)['"]\]|\.(\w+))\.(\w+)\s*\(/g
492
+ ))
493
+ out.push({
494
+ callType: "local_service_call",
495
+ operationPathExpr: `/${m[3] ?? ""}`,
496
+ payloadSummary: m[1] ?? m[2],
497
+ sourceFile: normalizePath(filePath),
498
+ sourceLine: lineOf3(text, m.index ?? 0),
499
+ confidence: 0.75
500
+ });
501
+ return out;
502
+ }
503
+
504
+ // src/parsers/service-binding-parser.ts
505
+ import fs7 from "fs/promises";
506
+ import path8 from "path";
507
+ import ts3 from "typescript";
508
+ function lineOf4(sf, node) {
509
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
510
+ }
511
+ function stringValue(node) {
512
+ if (!node) return void 0;
513
+ if (ts3.isStringLiteralLike(node) || ts3.isNoSubstitutionTemplateLiteral(node))
514
+ return node.text;
515
+ if (ts3.isTemplateExpression(node))
516
+ return node.getText().replace(/^`|`$/g, "");
517
+ return node.getText();
518
+ }
519
+ function placeholders(value) {
520
+ return [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((m) => m[1] ?? "").filter(Boolean);
521
+ }
522
+ function connectFactFromCall(call) {
523
+ const expr = call.expression;
524
+ if (!ts3.isPropertyAccessExpression(expr) || expr.name.text !== "to")
525
+ return void 0;
526
+ const inner = expr.expression;
527
+ if (!ts3.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
528
+ return void 0;
529
+ const first = call.arguments[0];
530
+ if (!first) return void 0;
531
+ const second = call.arguments[1];
532
+ const objectArg = ts3.isObjectLiteralExpression(first) ? first : second && ts3.isObjectLiteralExpression(second) ? second : void 0;
533
+ let alias;
534
+ let aliasExpr;
535
+ if (ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first))
536
+ alias = first.text;
537
+ else if (!ts3.isObjectLiteralExpression(first))
538
+ aliasExpr = stringValue(first);
539
+ if ((ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
540
+ return { alias: first.text, isDynamic: false, placeholders: [] };
541
+ if (!objectArg && aliasExpr)
542
+ return {
543
+ aliasExpr,
544
+ isDynamic: true,
545
+ placeholders: placeholders(aliasExpr)
546
+ };
547
+ let destinationExpr;
548
+ let servicePathExpr;
549
+ function visitObject(obj) {
550
+ for (const prop of obj.properties) {
551
+ if (!ts3.isPropertyAssignment(prop)) continue;
552
+ const name = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
553
+ if (name === "destination")
554
+ destinationExpr = stringValue(prop.initializer);
555
+ if (name === "path" || name === "servicePath")
556
+ servicePathExpr = stringValue(prop.initializer);
557
+ if (ts3.isObjectLiteralExpression(prop.initializer))
558
+ visitObject(prop.initializer);
559
+ }
560
+ }
561
+ if (objectArg) visitObject(objectArg);
562
+ const ph = [
563
+ ...placeholders(aliasExpr ?? alias),
564
+ ...placeholders(destinationExpr),
565
+ ...placeholders(servicePathExpr)
566
+ ];
567
+ return {
568
+ alias,
569
+ aliasExpr,
570
+ destinationExpr,
571
+ servicePathExpr,
572
+ isDynamic: ph.length > 0 || !destinationExpr && !servicePathExpr,
573
+ placeholders: ph
574
+ };
575
+ }
576
+ function unwrapCall(expr) {
577
+ if (ts3.isAwaitExpression(expr)) return unwrapCall(expr.expression);
578
+ if (ts3.isCallExpression(expr)) return expr;
579
+ return void 0;
580
+ }
581
+ function findConnectInExpression(expr) {
582
+ const direct = unwrapCall(expr);
583
+ if (direct) {
584
+ const fact = connectFactFromCall(direct);
585
+ if (fact) return fact;
586
+ }
587
+ let found;
588
+ function visit(node) {
589
+ if (found) return;
590
+ if (ts3.isCallExpression(node)) found = connectFactFromCall(node);
591
+ if (!found) ts3.forEachChild(node, visit);
592
+ }
593
+ visit(expr);
594
+ return found;
595
+ }
596
+ async function readSource(abs) {
597
+ try {
598
+ const text = await fs7.readFile(abs, "utf8");
599
+ return ts3.createSourceFile(
600
+ abs,
601
+ text,
602
+ ts3.ScriptTarget.Latest,
603
+ true,
604
+ ts3.ScriptKind.TS
605
+ );
606
+ } catch {
607
+ return void 0;
608
+ }
609
+ }
610
+ async function resolveImport(repoPath, fromFile, spec) {
611
+ if (!spec.startsWith(".")) return void 0;
612
+ const rawBase = path8.resolve(repoPath, path8.dirname(fromFile), spec);
613
+ const parsed = path8.parse(rawBase);
614
+ const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path8.join(parsed.dir, parsed.name) : rawBase;
615
+ for (const candidate of [
616
+ base,
617
+ `${base}.ts`,
618
+ `${base}.js`,
619
+ path8.join(base, "index.ts"),
620
+ path8.join(base, "index.js")
621
+ ]) {
622
+ try {
623
+ const st = await fs7.stat(candidate);
624
+ if (st.isFile()) return normalizePath(path8.relative(repoPath, candidate));
625
+ } catch {
626
+ }
627
+ }
628
+ return void 0;
629
+ }
630
+ async function importsFor(repoPath, filePath, sf) {
631
+ const imports = [];
632
+ for (const stmt of sf.statements) {
633
+ if (!ts3.isImportDeclaration(stmt) || !ts3.isStringLiteralLike(stmt.moduleSpecifier))
634
+ continue;
635
+ const sourceFile = await resolveImport(
636
+ repoPath,
637
+ filePath,
638
+ stmt.moduleSpecifier.text
639
+ );
640
+ const clause = stmt.importClause;
641
+ if (!clause) continue;
642
+ if (clause.name)
643
+ imports.push({
644
+ localName: clause.name.text,
645
+ exportedName: "default",
646
+ sourceFile
647
+ });
648
+ const bindings = clause.namedBindings;
649
+ if (bindings && ts3.isNamedImports(bindings))
650
+ for (const el of bindings.elements)
651
+ imports.push({
652
+ localName: el.name.text,
653
+ exportedName: el.propertyName?.text ?? el.name.text,
654
+ sourceFile
655
+ });
656
+ }
657
+ return imports;
658
+ }
659
+ function exportedLocalNames(sf) {
660
+ const exports = /* @__PURE__ */ new Map();
661
+ for (const stmt of sf.statements) {
662
+ const direct = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
663
+ (m) => m.kind === ts3.SyntaxKind.ExportKeyword
664
+ ) ?? false : false;
665
+ if (direct && ts3.isFunctionDeclaration(stmt) && stmt.name)
666
+ exports.set(stmt.name.text, stmt.name.text);
667
+ if (direct && ts3.isVariableStatement(stmt)) {
668
+ for (const decl of stmt.declarationList.declarations)
669
+ if (ts3.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
670
+ }
671
+ if (!ts3.isExportDeclaration(stmt) || !stmt.exportClause) continue;
672
+ if (!ts3.isNamedExports(stmt.exportClause)) continue;
673
+ for (const el of stmt.exportClause.elements)
674
+ exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
675
+ }
676
+ return exports;
677
+ }
678
+ async function helperBindings(repoPath, filePath) {
679
+ const sf = await readSource(path8.join(repoPath, filePath));
680
+ if (!sf) return [];
681
+ const sourceFileAst = sf;
682
+ const out = [];
683
+ const exportedLocals = exportedLocalNames(sf);
684
+ const factsByLocal = /* @__PURE__ */ new Map();
685
+ for (const stmt of sf.statements) {
686
+ if (ts3.isFunctionDeclaration(stmt) && stmt.name) {
687
+ let fact;
688
+ stmt.forEachChild(function visit(node) {
689
+ if (!fact && ts3.isReturnStatement(node) && node.expression)
690
+ fact = findConnectInExpression(node.expression);
691
+ if (!fact) ts3.forEachChild(node, visit);
692
+ });
693
+ if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
694
+ }
695
+ if (ts3.isVariableStatement(stmt))
696
+ for (const decl of stmt.declarationList.declarations) {
697
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
698
+ const fact = findConnectInExpression(decl.initializer);
699
+ if (fact)
700
+ factsByLocal.set(decl.name.text, {
701
+ ...fact,
702
+ sourceLine: lineOf4(sourceFileAst, decl)
703
+ });
704
+ }
705
+ }
706
+ for (const [exportedName, localName] of exportedLocals) {
707
+ const fact = factsByLocal.get(localName);
708
+ if (fact)
709
+ out.push({
710
+ ...fact,
711
+ exportedName,
712
+ sourceFile: normalizePath(filePath),
713
+ sourceLine: fact.sourceLine
714
+ });
715
+ }
716
+ return out;
717
+ }
718
+ async function parseServiceBindings(repoPath, filePath) {
719
+ const sf = await readSource(path8.join(repoPath, filePath));
720
+ if (!sf) return [];
721
+ const sourceFileAst = sf;
722
+ const out = [];
723
+ const imports = await importsFor(repoPath, filePath, sf);
724
+ const helperCache = /* @__PURE__ */ new Map();
725
+ const classHelpers = collectClassHelpers(sourceFileAst);
726
+ async function importedHelper(localName) {
727
+ const imp = imports.find((i) => i.localName === localName && i.sourceFile);
728
+ if (!imp?.sourceFile) return void 0;
729
+ if (!helperCache.has(imp.sourceFile))
730
+ helperCache.set(
731
+ imp.sourceFile,
732
+ await helperBindings(repoPath, imp.sourceFile)
733
+ );
734
+ const helper = helperCache.get(imp.sourceFile)?.find((h) => h.exportedName === imp.exportedName);
735
+ return helper ? { imp, helper } : void 0;
736
+ }
737
+ async function recordVariable(decl) {
738
+ if (!ts3.isIdentifier(decl.name) || !decl.initializer) return;
739
+ const call = unwrapCall(decl.initializer);
740
+ if (!call) return;
741
+ const direct = connectFactFromCall(call);
742
+ if (direct)
743
+ out.push({
744
+ variableName: decl.name.text,
745
+ ...direct,
746
+ sourceFile: normalizePath(filePath),
747
+ sourceLine: lineOf4(sourceFileAst, decl)
748
+ });
749
+ else if (ts3.isIdentifier(call.expression)) {
750
+ const resolved = await importedHelper(call.expression.text);
751
+ if (resolved)
752
+ out.push({
753
+ variableName: decl.name.text,
754
+ alias: resolved.helper.alias,
755
+ aliasExpr: resolved.helper.aliasExpr,
756
+ destinationExpr: resolved.helper.destinationExpr,
757
+ servicePathExpr: resolved.helper.servicePathExpr,
758
+ isDynamic: resolved.helper.isDynamic,
759
+ placeholders: resolved.helper.placeholders,
760
+ sourceFile: normalizePath(filePath),
761
+ sourceLine: lineOf4(sourceFileAst, decl),
762
+ helperChain: [
763
+ {
764
+ callerVariable: decl.name.text,
765
+ importedHelper: call.expression.text,
766
+ importSource: resolved.imp.sourceFile,
767
+ exportedSymbol: resolved.imp.exportedName,
768
+ helperSourceFile: resolved.helper.sourceFile,
769
+ helperSourceLine: resolved.helper.sourceLine
770
+ }
771
+ ]
772
+ });
773
+ }
774
+ }
775
+ function recordDestructuredClassHelper(decl) {
776
+ if (!ts3.isObjectBindingPattern(decl.name) || !decl.initializer) return;
777
+ const call = unwrapCall(decl.initializer);
778
+ if (!call || !ts3.isPropertyAccessExpression(call.expression)) return;
779
+ const target = call.expression;
780
+ if (target.expression.kind !== ts3.SyntaxKind.ThisKeyword) return;
781
+ for (const el of decl.name.elements) {
782
+ if (!ts3.isIdentifier(el.name)) continue;
783
+ const propertyName = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
784
+ const helper = classHelpers.find(
785
+ (h) => h.helperName === target.name.text && h.propertyName === propertyName
786
+ );
787
+ if (!helper) continue;
788
+ out.push({
789
+ variableName: el.name.text,
790
+ ...helper.fact,
791
+ sourceFile: normalizePath(filePath),
792
+ sourceLine: lineOf4(sourceFileAst, decl),
793
+ helperChain: [
794
+ {
795
+ callerVariable: el.name.text,
796
+ className: helper.className,
797
+ classHelper: helper.helperName,
798
+ returnedProperty: helper.propertyName,
799
+ helperVariable: helper.variableName,
800
+ helperSourceFile: normalizePath(filePath),
801
+ helperSourceLine: helper.sourceLine
802
+ }
803
+ ]
804
+ });
805
+ }
806
+ }
807
+ const declarations = [];
808
+ function collectDeclarations(node) {
809
+ if (ts3.isVariableDeclaration(node)) declarations.push(node);
810
+ ts3.forEachChild(node, collectDeclarations);
811
+ }
812
+ collectDeclarations(sourceFileAst);
813
+ for (const decl of declarations) {
814
+ recordDestructuredClassHelper(decl);
815
+ await recordVariable(decl);
816
+ }
817
+ return out;
818
+ }
819
+ function collectClassHelpers(sf) {
820
+ const helpers = [];
821
+ for (const stmt of sf.statements) {
822
+ if (!ts3.isClassDeclaration(stmt) || !stmt.name) continue;
823
+ for (const member of stmt.members) {
824
+ let visit2 = function(node) {
825
+ if (ts3.isVariableDeclaration(node) && ts3.isIdentifier(node.name) && node.initializer) {
826
+ const fact = findConnectInExpression(node.initializer);
827
+ if (fact) bindings.set(node.name.text, fact);
828
+ }
829
+ if (ts3.isReturnStatement(node) && node.expression && ts3.isObjectLiteralExpression(node.expression)) {
830
+ for (const prop of node.expression.properties) {
831
+ if (ts3.isShorthandPropertyAssignment(prop)) {
832
+ const fact = bindings.get(prop.name.text);
833
+ if (fact)
834
+ helpers.push({
835
+ className,
836
+ helperName,
837
+ propertyName: prop.name.text,
838
+ variableName: prop.name.text,
839
+ fact,
840
+ sourceLine: lineOf4(sf, prop)
841
+ });
842
+ }
843
+ if (ts3.isPropertyAssignment(prop) && ts3.isIdentifier(prop.initializer)) {
844
+ const propertyName = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
845
+ const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
846
+ if (propertyName && fact)
847
+ helpers.push({
848
+ className,
849
+ helperName,
850
+ propertyName,
851
+ variableName: prop.initializer.text,
852
+ fact,
853
+ sourceLine: lineOf4(sf, prop)
854
+ });
855
+ }
856
+ }
857
+ }
858
+ ts3.forEachChild(node, visit2);
859
+ };
860
+ var visit = visit2;
861
+ if (!ts3.isPropertyDeclaration(member) || !member.initializer) continue;
862
+ if (!ts3.isIdentifier(member.name)) continue;
863
+ const className = stmt.name.text;
864
+ const helperName = member.name.text;
865
+ const initializer = member.initializer;
866
+ if (!ts3.isArrowFunction(initializer) && !ts3.isFunctionExpression(initializer))
867
+ continue;
868
+ const bindings = /* @__PURE__ */ new Map();
869
+ visit2(initializer);
870
+ }
871
+ }
872
+ return helpers;
873
+ }
874
+
875
+ // src/linker/dynamic-edge-resolver.ts
876
+ function applyVariables(template, vars) {
877
+ if (!template) return void 0;
878
+ return template.replace(
879
+ /\$\{\s*(\w+)\s*\}/g,
880
+ (_m, key) => vars[key] ?? `\${${key}}`
881
+ );
882
+ }
883
+
884
+ // src/linker/service-resolver.ts
885
+ function rows(db, operationPath, workspaceId) {
886
+ return db.prepare(
887
+ `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,0 score,'' reasons
888
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
889
+ WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
890
+ ).all(
891
+ workspaceId,
892
+ workspaceId,
893
+ operationPath,
894
+ operationPath.replace(/^\//, "")
895
+ );
896
+ }
897
+ function resolveOperation(db, signals, workspaceId) {
898
+ if (!signals.operationPath)
899
+ return {
900
+ status: "unresolved",
901
+ candidates: [],
902
+ reasons: ["missing_operation_path"]
903
+ };
904
+ const candidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
905
+ ...c,
906
+ score: 0.2,
907
+ reasons: ["operation_path_match"]
908
+ }));
909
+ if (candidates.length === 0)
910
+ return {
911
+ status: "unresolved",
912
+ candidates: [],
913
+ reasons: ["no_operation_candidates"]
914
+ };
915
+ const hasStrongSignal = Boolean(
916
+ signals.servicePath || signals.alias || signals.destination || signals.hasExplicitOverride
917
+ );
918
+ for (const c of candidates) {
919
+ if (signals.servicePath && c.servicePath === signals.servicePath) {
920
+ c.score += 0.75;
921
+ c.reasons.push("exact_service_path");
922
+ }
923
+ if (signals.servicePath && c.servicePath !== signals.servicePath) {
924
+ c.score -= 0.1;
925
+ c.reasons.push("service_path_mismatch");
926
+ }
927
+ if (signals.hasExplicitOverride) {
928
+ c.score += 0.2;
929
+ c.reasons.push("explicit_dynamic_override");
930
+ }
931
+ }
932
+ candidates.sort(
933
+ (a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName)
934
+ );
935
+ const best = candidates[0];
936
+ const second = candidates[1];
937
+ if (signals.isDynamic && !signals.hasExplicitOverride && !signals.servicePath)
938
+ return {
939
+ status: "dynamic",
940
+ candidates,
941
+ reasons: ["dynamic_target_without_override"]
942
+ };
943
+ if (!hasStrongSignal)
944
+ return {
945
+ status: candidates.length > 1 ? "ambiguous" : "unresolved",
946
+ candidates,
947
+ reasons: ["operation_path_only_has_no_strong_target_signal"]
948
+ };
949
+ if (best && best.score >= 0.9 && (!second || best.score - second.score >= 0.25))
950
+ return {
951
+ status: "resolved",
952
+ target: best,
953
+ candidates,
954
+ reasons: best.reasons
955
+ };
956
+ return {
957
+ status: candidates.length > 1 ? "ambiguous" : "unresolved",
958
+ candidates,
959
+ reasons: ["candidate_score_below_resolution_threshold"]
960
+ };
961
+ }
962
+
963
+ // src/linker/helper-package-linker.ts
964
+ function linkHelperPackages(db, workspaceId) {
965
+ const repos = db.prepare(
966
+ "SELECT id,name,dependencies_json FROM repositories WHERE workspace_id=?"
967
+ ).all(workspaceId);
968
+ let count = 0;
969
+ for (const repo of repos) {
970
+ const deps = JSON.parse(repo.dependencies_json);
971
+ for (const dep of Object.keys(deps)) {
972
+ const helper = repos.find((r) => r.name === dep || dep.endsWith(r.name));
973
+ if (helper) {
974
+ db.prepare(
975
+ "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?)"
976
+ ).run(
977
+ workspaceId,
978
+ "REPO_IMPORTS_HELPER_PACKAGE",
979
+ "repo",
980
+ String(repo.id),
981
+ "repo",
982
+ String(helper.id),
983
+ 0.9,
984
+ JSON.stringify({ dependency: dep }),
985
+ 0
986
+ );
987
+ count += 1;
988
+ }
989
+ }
990
+ }
991
+ return count;
992
+ }
993
+
994
+ // src/linker/cross-repo-linker.ts
995
+ function linkWorkspace(db, workspaceId, vars = {}) {
996
+ db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
997
+ let edges = linkHelperPackages(db, workspaceId);
998
+ let unresolved = 0;
999
+ const calls = db.prepare(
1000
+ `SELECT c.*,r.name repoName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,b.helper_chain_json helperChainJson,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=?`
1001
+ ).all(workspaceId);
1002
+ for (const call of calls) {
1003
+ const callType = String(call.call_type);
1004
+ const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
1005
+ const servicePath = applyVariables(
1006
+ call.servicePathExpr ?? call.requireServicePath,
1007
+ vars
1008
+ );
1009
+ const destination = call.destinationExpr ?? call.requireDestination;
1010
+ const isDynamic = Boolean(Number(call.isDynamic ?? 0));
1011
+ const resolution = callType.startsWith("remote") ? resolveOperation(
1012
+ db,
1013
+ {
1014
+ servicePath,
1015
+ operationPath: op,
1016
+ alias: call.alias,
1017
+ destination,
1018
+ isDynamic,
1019
+ hasExplicitOverride: Object.keys(vars).length > 0
1020
+ },
1021
+ workspaceId
1022
+ ) : { status: "unresolved", candidates: [], reasons: [] };
1023
+ const target = resolution.target;
1024
+ const evidence = {
1025
+ sourceFile: call.source_file,
1026
+ sourceLine: call.source_line,
1027
+ file: call.source_file,
1028
+ line: call.source_line,
1029
+ repo: call.repoName,
1030
+ serviceAlias: call.alias,
1031
+ serviceAliasExpr: call.aliasExpr,
1032
+ destination,
1033
+ servicePath,
1034
+ operationPath: op,
1035
+ targetRepo: target?.repoName,
1036
+ targetOperation: target?.operationName,
1037
+ helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
1038
+ candidates: resolution.candidates,
1039
+ candidateCount: resolution.candidates.length,
1040
+ resolutionStatus: resolution.status,
1041
+ resolutionReasons: resolution.reasons
1042
+ };
1043
+ if (target) {
1044
+ db.prepare(
1045
+ "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?)"
1046
+ ).run(
1047
+ workspaceId,
1048
+ "REMOTE_CALL_RESOLVES_TO_OPERATION",
1049
+ "call",
1050
+ String(call.id),
1051
+ "operation",
1052
+ String(target.operationId),
1053
+ target.score,
1054
+ JSON.stringify(evidence),
1055
+ isDynamic ? 1 : 0
1056
+ );
1057
+ edges += 1;
1058
+ } else {
1059
+ 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" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
1060
+ db.prepare(
1061
+ "INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?)"
1062
+ ).run(
1063
+ workspaceId,
1064
+ edgeType,
1065
+ "call",
1066
+ String(call.id),
1067
+ callType.startsWith("async_") ? "event" : "external",
1068
+ String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
1069
+ Number(call.confidence ?? 0.2),
1070
+ JSON.stringify(evidence),
1071
+ isDynamic || resolution.status === "dynamic" ? 1 : 0,
1072
+ String(
1073
+ call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? "Dynamic target requires runtime variable overrides" : "No indexed target operation matched")
1074
+ )
1075
+ );
1076
+ edges += 1;
1077
+ unresolved += edgeType === "UNRESOLVED_EDGE" ? 1 : 0;
1078
+ }
1079
+ }
1080
+ return { edgeCount: edges, unresolvedCount: unresolved };
1081
+ }
1082
+
1083
+ // src/trace/trace-engine.ts
1084
+ function normalizeOperation(value) {
1085
+ if (!value) return void 0;
1086
+ return value.startsWith("/") ? value.slice(1) : value;
1087
+ }
1088
+ function positiveDepth(value) {
1089
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
1090
+ }
1091
+ function sourceFilesForStart(db, repoId, start) {
1092
+ const handler = start.handler;
1093
+ const operation = normalizeOperation(start.operation ?? start.operationPath);
1094
+ if (!handler && !operation) return void 0;
1095
+ const rows2 = db.prepare(
1096
+ `SELECT DISTINCT hc.source_file sourceFile
1097
+ FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
1098
+ WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
1099
+ AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)`
1100
+ ).all(
1101
+ repoId,
1102
+ repoId,
1103
+ handler,
1104
+ handler,
1105
+ handler,
1106
+ operation,
1107
+ operation,
1108
+ operation
1109
+ );
1110
+ if (rows2.length === 0) return void 0;
1111
+ return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1112
+ }
1113
+ function startScope(db, start) {
1114
+ const repo = start.repo ? db.prepare(
1115
+ "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
1116
+ ).get(start.repo, start.repo) : void 0;
1117
+ if (start.repo && !repo) return { repo, selectorMatched: false };
1118
+ const sourceFiles = sourceFilesForStart(db, repo?.id, start);
1119
+ const hasSelector = Boolean(
1120
+ start.handler ?? start.operation ?? start.operationPath
1121
+ );
1122
+ return {
1123
+ repo,
1124
+ sourceFiles,
1125
+ selectorMatched: !hasSelector || sourceFiles !== void 0
1126
+ };
1127
+ }
1128
+ function handlerFilesForOperation(db, operationId) {
1129
+ const op = db.prepare(
1130
+ `SELECT o.operation_name operationName,o.operation_path operationPath,s.repo_id repoId
1131
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`
1132
+ ).get(operationId);
1133
+ if (!op) return /* @__PURE__ */ new Set();
1134
+ const operation = normalizeOperation(op.operationPath ?? op.operationName);
1135
+ const rows2 = db.prepare(
1136
+ `SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
1137
+ JOIN handler_methods hm ON hm.handler_class_id=hc.id
1138
+ WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
1139
+ ).all(op.repoId, operation, operation, op.operationName);
1140
+ return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1141
+ }
1142
+ function includeCall(type, options) {
1143
+ if (!options.includeDb && type === "local_db_query") return false;
1144
+ if (!options.includeExternal && type === "external_http") return false;
1145
+ if (!options.includeAsync && type.startsWith("async_")) return false;
1146
+ return true;
1147
+ }
1148
+ function graphForCalls(db, callIds) {
1149
+ const map = /* @__PURE__ */ new Map();
1150
+ if (callIds.length === 0) return map;
1151
+ const rows2 = db.prepare(
1152
+ `SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
1153
+ ).all(...callIds);
1154
+ for (const row of rows2) {
1155
+ const id = Number(row.from_id);
1156
+ map.set(id, [...map.get(id) ?? [], row]);
1157
+ }
1158
+ return map;
1159
+ }
1160
+ function candidatesFromEvidence(evidence) {
1161
+ return Array.isArray(evidence.candidates) ? evidence.candidates.filter(
1162
+ (candidate) => typeof candidate === "object" && candidate !== null
1163
+ ) : [];
1164
+ }
1165
+ function evidenceWithRuntimeVariables(evidence, vars) {
1166
+ if (!vars || Object.keys(vars).length === 0) return evidence;
1167
+ const servicePath = typeof evidence.servicePath === "string" ? applyVariables(evidence.servicePath, vars) : void 0;
1168
+ const operationPath = typeof evidence.operationPath === "string" ? applyVariables(evidence.operationPath, vars) : void 0;
1169
+ const candidates = candidatesFromEvidence(evidence);
1170
+ const matched = servicePath ? candidates.find((candidate) => candidate.servicePath === servicePath) : void 0;
1171
+ return {
1172
+ ...evidence,
1173
+ servicePath: servicePath ?? evidence.servicePath,
1174
+ operationPath: operationPath ?? evidence.operationPath,
1175
+ runtimeVariablesApplied: true,
1176
+ runtimeResolvedCandidate: matched ? {
1177
+ repoName: matched.repoName,
1178
+ servicePath: matched.servicePath,
1179
+ operationPath: matched.operationPath,
1180
+ operationName: matched.operationName,
1181
+ score: matched.score
1182
+ } : void 0
1183
+ };
1184
+ }
1185
+ function edgeTarget(row, evidence) {
1186
+ const runtimeCandidate = evidence.runtimeResolvedCandidate;
1187
+ if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
1188
+ return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
1189
+ const servicePath = typeof evidence.servicePath === "string" ? evidence.servicePath : void 0;
1190
+ const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
1191
+ const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
1192
+ const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
1193
+ return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
1194
+ }
1195
+ function trace(db, start, options) {
1196
+ const scope = startScope(db, start);
1197
+ const diagnostics = db.prepare(
1198
+ "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
1199
+ ).all(scope.repo?.id, scope.repo?.id);
1200
+ if (!scope.selectorMatched)
1201
+ diagnostics.unshift({
1202
+ severity: "warning",
1203
+ code: "trace_start_not_found",
1204
+ message: "No handler source matched the requested trace start selector"
1205
+ });
1206
+ const maxDepth = positiveDepth(options.depth);
1207
+ const edges = [];
1208
+ const nodes = /* @__PURE__ */ new Map();
1209
+ const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
1210
+ const seenScopes = /* @__PURE__ */ new Set();
1211
+ const seenEdges = /* @__PURE__ */ new Set();
1212
+ while (queue.length > 0) {
1213
+ const current = queue.shift();
1214
+ if (!current || current.depth > maxDepth) continue;
1215
+ const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
1216
+ if (seenScopes.has(key)) continue;
1217
+ seenScopes.add(key);
1218
+ const calls = db.prepare(
1219
+ `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`
1220
+ ).all(current.repoId, current.repoId);
1221
+ const filtered = calls.filter(
1222
+ (c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
1223
+ );
1224
+ const graph = graphForCalls(
1225
+ db,
1226
+ filtered.map((c) => Number(c.id))
1227
+ );
1228
+ for (const call of filtered) {
1229
+ const callNode = `call:${call.id}`;
1230
+ nodes.set(callNode, {
1231
+ id: callNode,
1232
+ kind: "outbound_call",
1233
+ repo: call.repoName,
1234
+ file: call.source_file,
1235
+ line: call.source_line,
1236
+ callType: call.call_type
1237
+ });
1238
+ const graphRows = graph.get(Number(call.id)) ?? [];
1239
+ for (const row of graphRows) {
1240
+ if (seenEdges.has(Number(row.id))) continue;
1241
+ seenEdges.add(Number(row.id));
1242
+ const rawEvidence = JSON.parse(
1243
+ String(row.evidence_json || "{}")
1244
+ );
1245
+ const evidence = evidenceWithRuntimeVariables(
1246
+ rawEvidence,
1247
+ options.vars
1248
+ );
1249
+ const targetNode = `${row.to_kind}:${row.to_id}`;
1250
+ nodes.set(targetNode, {
1251
+ id: targetNode,
1252
+ kind: row.to_kind,
1253
+ label: row.to_id,
1254
+ ...evidence
1255
+ });
1256
+ const to = edgeTarget(row, evidence);
1257
+ edges.push({
1258
+ step: current.depth,
1259
+ type: String(call.call_type),
1260
+ from: `${call.repoName}:${call.source_file}`,
1261
+ to,
1262
+ evidence,
1263
+ confidence: Number(row.confidence ?? call.confidence),
1264
+ unresolvedReason: row.unresolved_reason
1265
+ });
1266
+ if (row.to_kind === "operation" && current.depth < maxDepth) {
1267
+ const files = handlerFilesForOperation(db, row.to_id);
1268
+ if (files.size > 0) {
1269
+ const targetRepoId = db.prepare(
1270
+ "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1271
+ ).get(row.to_id)?.repoId;
1272
+ const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
1273
+ if (seenScopes.has(nextKey))
1274
+ edges.push({
1275
+ step: current.depth + 1,
1276
+ type: "cycle",
1277
+ from: to,
1278
+ to: nextKey,
1279
+ evidence: { ...evidence, cycle: true },
1280
+ confidence: 1,
1281
+ unresolvedReason: "Cycle detected; downstream scope already visited"
1282
+ });
1283
+ else
1284
+ queue.push({
1285
+ repoId: targetRepoId,
1286
+ files,
1287
+ depth: current.depth + 1
1288
+ });
1289
+ }
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+ return { start, nodes: [...nodes.values()], edges, diagnostics };
1295
+ }
1296
+
1297
+ export {
1298
+ normalizePath,
1299
+ stripQuotes,
1300
+ discoverRepositories,
1301
+ parsePackageJson,
1302
+ parseCdsFile,
1303
+ parseDecorators,
1304
+ parseHandlerRegistrations,
1305
+ redactText,
1306
+ redactValue,
1307
+ parseOutboundCalls,
1308
+ parseServiceBindings,
1309
+ applyVariables,
1310
+ linkWorkspace,
1311
+ trace
1312
+ };
1313
+ //# sourceMappingURL=chunk-JY6GBGZT.js.map