@saptools/service-flow 0.1.2 → 0.1.3
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 +9 -0
- package/README.md +8 -7
- package/TECHNICAL-NOTE.md +7 -0
- package/dist/{chunk-6U4QKQEL.js → chunk-SIKIGT42.js} +542 -118
- package/dist/chunk-SIKIGT42.js.map +1 -0
- package/dist/cli.js +57 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +2 -1
- package/dist/chunk-6U4QKQEL.js.map +0 -1
|
@@ -369,34 +369,81 @@ function lineOf3(text, idx) {
|
|
|
369
369
|
function firstArg2(body, key) {
|
|
370
370
|
return new RegExp(`${key}\\s*:\\s*([^,}\\n]+)`).exec(body)?.[1]?.trim();
|
|
371
371
|
}
|
|
372
|
+
function matchingParen(text, open) {
|
|
373
|
+
let depth = 0;
|
|
374
|
+
let quote;
|
|
375
|
+
let escaped = false;
|
|
376
|
+
for (let i = open; i < text.length; i += 1) {
|
|
377
|
+
const ch = text[i] ?? "";
|
|
378
|
+
if (quote) {
|
|
379
|
+
if (escaped) escaped = false;
|
|
380
|
+
else if (ch === "\\") escaped = true;
|
|
381
|
+
else if (ch === quote) quote = void 0;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
385
|
+
quote = ch;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (ch === "(") depth += 1;
|
|
389
|
+
if (ch === ")") {
|
|
390
|
+
depth -= 1;
|
|
391
|
+
if (depth === 0) return i;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return -1;
|
|
395
|
+
}
|
|
396
|
+
function argumentForCall(expr, marker) {
|
|
397
|
+
const idx = expr.indexOf(marker);
|
|
398
|
+
if (idx < 0) return void 0;
|
|
399
|
+
const open = expr.indexOf("(", idx + marker.length);
|
|
400
|
+
if (open < 0) return void 0;
|
|
401
|
+
const close = matchingParen(expr, open);
|
|
402
|
+
return close > open ? expr.slice(open + 1, close).trim() : void 0;
|
|
403
|
+
}
|
|
404
|
+
function entityFromArg(arg) {
|
|
405
|
+
if (!arg) return void 0;
|
|
406
|
+
const first = arg.split(",")[0]?.trim();
|
|
407
|
+
if (!first) return void 0;
|
|
408
|
+
return stripQuotes(first).replace(/^this\./, "");
|
|
409
|
+
}
|
|
410
|
+
function extractQueryEntity(expr) {
|
|
411
|
+
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"));
|
|
412
|
+
}
|
|
372
413
|
async function parseOutboundCalls(repoPath, filePath) {
|
|
373
414
|
const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
|
|
374
415
|
const out = [];
|
|
375
416
|
for (const m of text.matchAll(/(\w+)\.send\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
|
|
376
417
|
const body = m[2] ?? "";
|
|
377
418
|
const query = firstArg2(body, "query");
|
|
378
|
-
const op = firstArg2(body, "path");
|
|
419
|
+
const op = firstArg2(body, "path") ?? firstArg2(body, "event");
|
|
379
420
|
out.push({
|
|
380
421
|
callType: query ? "remote_query" : "remote_action",
|
|
381
422
|
serviceVariableName: m[1],
|
|
382
423
|
method: stripQuotes(firstArg2(body, "method") ?? "POST"),
|
|
383
|
-
operationPathExpr: op ? stripQuotes(op) : void 0,
|
|
384
|
-
queryEntity:
|
|
424
|
+
operationPathExpr: op ? `/${stripQuotes(op).replace(/^\//, "")}` : void 0,
|
|
425
|
+
queryEntity: extractQueryEntity(query ?? ""),
|
|
385
426
|
payloadSummary: summarizeExpression(body),
|
|
386
427
|
sourceFile: normalizePath(filePath),
|
|
387
428
|
sourceLine: lineOf3(text, m.index ?? 0),
|
|
388
429
|
confidence: op || query ? 0.8 : 0.4
|
|
389
430
|
});
|
|
390
431
|
}
|
|
391
|
-
for (const m of text.matchAll(/cds\.run\s*\(
|
|
432
|
+
for (const m of text.matchAll(/cds\.run\s*\(/g)) {
|
|
433
|
+
const open = (m.index ?? 0) + m[0].lastIndexOf("(");
|
|
434
|
+
const close = matchingParen(text, open);
|
|
435
|
+
const expr = close > open ? text.slice(open + 1, close) : "";
|
|
436
|
+
const entity = extractQueryEntity(expr);
|
|
392
437
|
out.push({
|
|
393
438
|
callType: "local_db_query",
|
|
394
|
-
queryEntity:
|
|
395
|
-
payloadSummary: summarizeExpression(
|
|
439
|
+
queryEntity: entity,
|
|
440
|
+
payloadSummary: summarizeExpression(expr),
|
|
396
441
|
sourceFile: normalizePath(filePath),
|
|
397
442
|
sourceLine: lineOf3(text, m.index ?? 0),
|
|
398
|
-
confidence: 0.
|
|
443
|
+
confidence: entity ? 0.9 : 0.55,
|
|
444
|
+
unresolvedReason: entity ? void 0 : "Could not resolve CAP query target entity from nested expression"
|
|
399
445
|
});
|
|
446
|
+
}
|
|
400
447
|
for (const m of text.matchAll(
|
|
401
448
|
/(\w+)\.(emit|publish|on)\s*\(\s*(['"`])([^'"`]+)\3/g
|
|
402
449
|
))
|
|
@@ -436,67 +483,246 @@ async function parseOutboundCalls(repoPath, filePath) {
|
|
|
436
483
|
// src/parsers/service-binding-parser.ts
|
|
437
484
|
import fs7 from "fs/promises";
|
|
438
485
|
import path8 from "path";
|
|
439
|
-
|
|
440
|
-
|
|
486
|
+
import ts3 from "typescript";
|
|
487
|
+
function lineOf4(sf, node) {
|
|
488
|
+
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
489
|
+
}
|
|
490
|
+
function stringValue(node) {
|
|
491
|
+
if (!node) return void 0;
|
|
492
|
+
if (ts3.isStringLiteralLike(node) || ts3.isNoSubstitutionTemplateLiteral(node))
|
|
493
|
+
return node.text;
|
|
494
|
+
if (ts3.isTemplateExpression(node))
|
|
495
|
+
return node.getText().replace(/^`|`$/g, "");
|
|
496
|
+
return node.getText();
|
|
441
497
|
}
|
|
442
498
|
function placeholders(value) {
|
|
443
499
|
return [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((m) => m[1] ?? "").filter(Boolean);
|
|
444
500
|
}
|
|
501
|
+
function connectFactFromCall(call) {
|
|
502
|
+
const expr = call.expression;
|
|
503
|
+
if (!ts3.isPropertyAccessExpression(expr) || expr.name.text !== "to")
|
|
504
|
+
return void 0;
|
|
505
|
+
const inner = expr.expression;
|
|
506
|
+
if (!ts3.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
|
|
507
|
+
return void 0;
|
|
508
|
+
const first = call.arguments[0];
|
|
509
|
+
if (!first) return void 0;
|
|
510
|
+
if (ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first))
|
|
511
|
+
return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
512
|
+
if (!ts3.isObjectLiteralExpression(first))
|
|
513
|
+
return {
|
|
514
|
+
servicePathExpr: first.getText(),
|
|
515
|
+
isDynamic: true,
|
|
516
|
+
placeholders: []
|
|
517
|
+
};
|
|
518
|
+
let destinationExpr;
|
|
519
|
+
let servicePathExpr;
|
|
520
|
+
function visitObject(obj) {
|
|
521
|
+
for (const prop of obj.properties) {
|
|
522
|
+
if (!ts3.isPropertyAssignment(prop)) continue;
|
|
523
|
+
const name = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
524
|
+
if (name === "destination")
|
|
525
|
+
destinationExpr = stringValue(prop.initializer);
|
|
526
|
+
if (name === "path" || name === "servicePath")
|
|
527
|
+
servicePathExpr = stringValue(prop.initializer);
|
|
528
|
+
if (ts3.isObjectLiteralExpression(prop.initializer))
|
|
529
|
+
visitObject(prop.initializer);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
visitObject(first);
|
|
533
|
+
const ph = [
|
|
534
|
+
...placeholders(destinationExpr),
|
|
535
|
+
...placeholders(servicePathExpr)
|
|
536
|
+
];
|
|
537
|
+
return {
|
|
538
|
+
destinationExpr,
|
|
539
|
+
servicePathExpr,
|
|
540
|
+
isDynamic: ph.length > 0 || !destinationExpr && !servicePathExpr,
|
|
541
|
+
placeholders: ph
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function unwrapCall(expr) {
|
|
545
|
+
if (ts3.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
546
|
+
if (ts3.isCallExpression(expr)) return expr;
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
function findConnectInExpression(expr) {
|
|
550
|
+
const direct = unwrapCall(expr);
|
|
551
|
+
if (direct) {
|
|
552
|
+
const fact = connectFactFromCall(direct);
|
|
553
|
+
if (fact) return fact;
|
|
554
|
+
}
|
|
555
|
+
let found;
|
|
556
|
+
function visit(node) {
|
|
557
|
+
if (found) return;
|
|
558
|
+
if (ts3.isCallExpression(node)) found = connectFactFromCall(node);
|
|
559
|
+
if (!found) ts3.forEachChild(node, visit);
|
|
560
|
+
}
|
|
561
|
+
visit(expr);
|
|
562
|
+
return found;
|
|
563
|
+
}
|
|
564
|
+
async function readSource(abs) {
|
|
565
|
+
try {
|
|
566
|
+
const text = await fs7.readFile(abs, "utf8");
|
|
567
|
+
return ts3.createSourceFile(
|
|
568
|
+
abs,
|
|
569
|
+
text,
|
|
570
|
+
ts3.ScriptTarget.Latest,
|
|
571
|
+
true,
|
|
572
|
+
ts3.ScriptKind.TS
|
|
573
|
+
);
|
|
574
|
+
} catch {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function resolveImport(repoPath, fromFile, spec) {
|
|
579
|
+
if (!spec.startsWith(".")) return void 0;
|
|
580
|
+
const base = path8.resolve(repoPath, path8.dirname(fromFile), spec);
|
|
581
|
+
for (const candidate of [
|
|
582
|
+
base,
|
|
583
|
+
`${base}.ts`,
|
|
584
|
+
`${base}.js`,
|
|
585
|
+
path8.join(base, "index.ts"),
|
|
586
|
+
path8.join(base, "index.js")
|
|
587
|
+
]) {
|
|
588
|
+
try {
|
|
589
|
+
const st = await fs7.stat(candidate);
|
|
590
|
+
if (st.isFile()) return normalizePath(path8.relative(repoPath, candidate));
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return void 0;
|
|
595
|
+
}
|
|
596
|
+
async function importsFor(repoPath, filePath, sf) {
|
|
597
|
+
const imports = [];
|
|
598
|
+
for (const stmt of sf.statements) {
|
|
599
|
+
if (!ts3.isImportDeclaration(stmt) || !ts3.isStringLiteralLike(stmt.moduleSpecifier))
|
|
600
|
+
continue;
|
|
601
|
+
const sourceFile = await resolveImport(
|
|
602
|
+
repoPath,
|
|
603
|
+
filePath,
|
|
604
|
+
stmt.moduleSpecifier.text
|
|
605
|
+
);
|
|
606
|
+
const clause = stmt.importClause;
|
|
607
|
+
if (!clause) continue;
|
|
608
|
+
if (clause.name)
|
|
609
|
+
imports.push({
|
|
610
|
+
localName: clause.name.text,
|
|
611
|
+
exportedName: "default",
|
|
612
|
+
sourceFile
|
|
613
|
+
});
|
|
614
|
+
const bindings = clause.namedBindings;
|
|
615
|
+
if (bindings && ts3.isNamedImports(bindings))
|
|
616
|
+
for (const el of bindings.elements)
|
|
617
|
+
imports.push({
|
|
618
|
+
localName: el.name.text,
|
|
619
|
+
exportedName: el.propertyName?.text ?? el.name.text,
|
|
620
|
+
sourceFile
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return imports;
|
|
624
|
+
}
|
|
625
|
+
async function helperBindings(repoPath, filePath) {
|
|
626
|
+
const sf = await readSource(path8.join(repoPath, filePath));
|
|
627
|
+
if (!sf) return [];
|
|
628
|
+
const sourceFileAst = sf;
|
|
629
|
+
const out = [];
|
|
630
|
+
for (const stmt of sf.statements) {
|
|
631
|
+
const exported = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
|
|
632
|
+
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
|
|
633
|
+
) ?? false : false;
|
|
634
|
+
if (ts3.isFunctionDeclaration(stmt) && stmt.name) {
|
|
635
|
+
let fact;
|
|
636
|
+
stmt.forEachChild(function visit(node) {
|
|
637
|
+
if (!fact && ts3.isReturnStatement(node) && node.expression)
|
|
638
|
+
fact = findConnectInExpression(node.expression);
|
|
639
|
+
if (!fact) ts3.forEachChild(node, visit);
|
|
640
|
+
});
|
|
641
|
+
if (fact && exported)
|
|
642
|
+
out.push({
|
|
643
|
+
...fact,
|
|
644
|
+
exportedName: stmt.name.text,
|
|
645
|
+
sourceFile: normalizePath(filePath),
|
|
646
|
+
sourceLine: lineOf4(sf, stmt)
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (ts3.isVariableStatement(stmt))
|
|
650
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
651
|
+
if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
652
|
+
const fact = findConnectInExpression(decl.initializer);
|
|
653
|
+
if (fact && exported)
|
|
654
|
+
out.push({
|
|
655
|
+
...fact,
|
|
656
|
+
exportedName: decl.name.text,
|
|
657
|
+
sourceFile: normalizePath(filePath),
|
|
658
|
+
sourceLine: lineOf4(sourceFileAst, decl)
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
445
664
|
async function parseServiceBindings(repoPath, filePath) {
|
|
446
|
-
const
|
|
665
|
+
const sf = await readSource(path8.join(repoPath, filePath));
|
|
666
|
+
if (!sf) return [];
|
|
667
|
+
const sourceFileAst = sf;
|
|
447
668
|
const out = [];
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
/(?:const|let|this\.)\s*(\w+)\s*=\s*(?:await\s*)?cds\.connect\.to\(\{([\s\S]*?)\}\s*\)/g
|
|
461
|
-
)) {
|
|
462
|
-
const body = m[2] ?? "";
|
|
463
|
-
const destination = /destination\s*:\s*([^,\n]+)/.exec(body)?.[1];
|
|
464
|
-
const servicePath = /path\s*:\s*([^,\n]+)/.exec(body)?.[1];
|
|
465
|
-
const dest = destination ? stripQuotes(destination.trim()) : void 0;
|
|
466
|
-
const svc = servicePath ? stripQuotes(servicePath.trim()) : void 0;
|
|
467
|
-
const ph = [...placeholders(dest), ...placeholders(svc)];
|
|
468
|
-
out.push({
|
|
469
|
-
variableName: m[1] ?? "service",
|
|
470
|
-
destinationExpr: dest,
|
|
471
|
-
servicePathExpr: svc,
|
|
472
|
-
isDynamic: ph.length > 0,
|
|
473
|
-
placeholders: ph,
|
|
474
|
-
sourceFile: normalizePath(filePath),
|
|
475
|
-
sourceLine: lineOf4(text, m.index ?? 0)
|
|
476
|
-
});
|
|
669
|
+
const imports = await importsFor(repoPath, filePath, sf);
|
|
670
|
+
const helperCache = /* @__PURE__ */ new Map();
|
|
671
|
+
async function importedHelper(localName) {
|
|
672
|
+
const imp = imports.find((i) => i.localName === localName && i.sourceFile);
|
|
673
|
+
if (!imp?.sourceFile) return void 0;
|
|
674
|
+
if (!helperCache.has(imp.sourceFile))
|
|
675
|
+
helperCache.set(
|
|
676
|
+
imp.sourceFile,
|
|
677
|
+
await helperBindings(repoPath, imp.sourceFile)
|
|
678
|
+
);
|
|
679
|
+
const helper = helperCache.get(imp.sourceFile)?.find((h) => h.exportedName === imp.exportedName);
|
|
680
|
+
return helper ? { imp, helper } : void 0;
|
|
477
681
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
682
|
+
async function recordVariable(decl) {
|
|
683
|
+
if (!ts3.isIdentifier(decl.name) || !decl.initializer) return;
|
|
684
|
+
const call = unwrapCall(decl.initializer);
|
|
685
|
+
if (!call) return;
|
|
686
|
+
const direct = connectFactFromCall(call);
|
|
687
|
+
if (direct)
|
|
688
|
+
out.push({
|
|
689
|
+
variableName: decl.name.text,
|
|
690
|
+
...direct,
|
|
691
|
+
sourceFile: normalizePath(filePath),
|
|
692
|
+
sourceLine: lineOf4(sourceFileAst, decl)
|
|
693
|
+
});
|
|
694
|
+
else if (ts3.isIdentifier(call.expression)) {
|
|
695
|
+
const resolved = await importedHelper(call.expression.text);
|
|
696
|
+
if (resolved)
|
|
697
|
+
out.push({
|
|
698
|
+
variableName: decl.name.text,
|
|
699
|
+
alias: resolved.helper.alias,
|
|
700
|
+
destinationExpr: resolved.helper.destinationExpr,
|
|
701
|
+
servicePathExpr: resolved.helper.servicePathExpr,
|
|
702
|
+
isDynamic: resolved.helper.isDynamic,
|
|
703
|
+
placeholders: resolved.helper.placeholders,
|
|
704
|
+
sourceFile: normalizePath(filePath),
|
|
705
|
+
sourceLine: lineOf4(sourceFileAst, decl),
|
|
706
|
+
helperChain: [
|
|
707
|
+
{
|
|
708
|
+
callerVariable: decl.name.text,
|
|
709
|
+
importedHelper: call.expression.text,
|
|
710
|
+
importSource: resolved.imp.sourceFile,
|
|
711
|
+
exportedSymbol: resolved.imp.exportedName,
|
|
712
|
+
helperSourceFile: resolved.helper.sourceFile,
|
|
713
|
+
helperSourceLine: resolved.helper.sourceLine
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const declarations = [];
|
|
720
|
+
function collectDeclarations(node) {
|
|
721
|
+
if (ts3.isVariableDeclaration(node)) declarations.push(node);
|
|
722
|
+
ts3.forEachChild(node, collectDeclarations);
|
|
499
723
|
}
|
|
724
|
+
collectDeclarations(sourceFileAst);
|
|
725
|
+
for (const decl of declarations) await recordVariable(decl);
|
|
500
726
|
return out;
|
|
501
727
|
}
|
|
502
728
|
|
|
@@ -510,28 +736,82 @@ function applyVariables(template, vars) {
|
|
|
510
736
|
}
|
|
511
737
|
|
|
512
738
|
// src/linker/service-resolver.ts
|
|
513
|
-
function
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
JOIN repositories r ON r.id=s.repo_id
|
|
520
|
-
WHERE (? IS NULL OR r.workspace_id=?)
|
|
521
|
-
AND (? IS NULL OR s.service_path=?)
|
|
522
|
-
AND (o.operation_path=? OR o.operation_name=?)
|
|
523
|
-
ORDER BY CASE WHEN s.service_path=? THEN 0 ELSE 1 END
|
|
524
|
-
LIMIT 1`
|
|
525
|
-
).get(
|
|
739
|
+
function rows(db, operationPath, workspaceId) {
|
|
740
|
+
return db.prepare(
|
|
741
|
+
`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
|
|
742
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
743
|
+
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`
|
|
744
|
+
).all(
|
|
526
745
|
workspaceId,
|
|
527
746
|
workspaceId,
|
|
528
|
-
servicePath,
|
|
529
|
-
servicePath,
|
|
530
747
|
operationPath,
|
|
531
|
-
operationPath.replace(/^\//, "")
|
|
532
|
-
|
|
748
|
+
operationPath.replace(/^\//, "")
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
function resolveOperation(db, signals, workspaceId) {
|
|
752
|
+
if (!signals.operationPath)
|
|
753
|
+
return {
|
|
754
|
+
status: "unresolved",
|
|
755
|
+
candidates: [],
|
|
756
|
+
reasons: ["missing_operation_path"]
|
|
757
|
+
};
|
|
758
|
+
const candidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
|
|
759
|
+
...c,
|
|
760
|
+
score: 0.2,
|
|
761
|
+
reasons: ["operation_path_match"]
|
|
762
|
+
}));
|
|
763
|
+
if (candidates.length === 0)
|
|
764
|
+
return {
|
|
765
|
+
status: "unresolved",
|
|
766
|
+
candidates: [],
|
|
767
|
+
reasons: ["no_operation_candidates"]
|
|
768
|
+
};
|
|
769
|
+
const hasStrongSignal = Boolean(
|
|
770
|
+
signals.servicePath || signals.alias || signals.destination || signals.hasExplicitOverride
|
|
771
|
+
);
|
|
772
|
+
for (const c of candidates) {
|
|
773
|
+
if (signals.servicePath && c.servicePath === signals.servicePath) {
|
|
774
|
+
c.score += 0.75;
|
|
775
|
+
c.reasons.push("exact_service_path");
|
|
776
|
+
}
|
|
777
|
+
if (signals.servicePath && c.servicePath !== signals.servicePath) {
|
|
778
|
+
c.score -= 0.1;
|
|
779
|
+
c.reasons.push("service_path_mismatch");
|
|
780
|
+
}
|
|
781
|
+
if (signals.hasExplicitOverride) {
|
|
782
|
+
c.score += 0.2;
|
|
783
|
+
c.reasons.push("explicit_dynamic_override");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
candidates.sort(
|
|
787
|
+
(a, b) => b.score - a.score || a.repoName.localeCompare(b.repoName)
|
|
533
788
|
);
|
|
534
|
-
|
|
789
|
+
const best = candidates[0];
|
|
790
|
+
const second = candidates[1];
|
|
791
|
+
if (signals.isDynamic && !signals.hasExplicitOverride && !signals.servicePath)
|
|
792
|
+
return {
|
|
793
|
+
status: "dynamic",
|
|
794
|
+
candidates,
|
|
795
|
+
reasons: ["dynamic_target_without_override"]
|
|
796
|
+
};
|
|
797
|
+
if (!hasStrongSignal)
|
|
798
|
+
return {
|
|
799
|
+
status: candidates.length > 1 ? "ambiguous" : "unresolved",
|
|
800
|
+
candidates,
|
|
801
|
+
reasons: ["operation_path_only_has_no_strong_target_signal"]
|
|
802
|
+
};
|
|
803
|
+
if (best && best.score >= 0.9 && (!second || best.score - second.score >= 0.25))
|
|
804
|
+
return {
|
|
805
|
+
status: "resolved",
|
|
806
|
+
target: best,
|
|
807
|
+
candidates,
|
|
808
|
+
reasons: best.reasons
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
status: candidates.length > 1 ? "ambiguous" : "unresolved",
|
|
812
|
+
candidates,
|
|
813
|
+
reasons: ["candidate_score_below_resolution_threshold"]
|
|
814
|
+
};
|
|
535
815
|
}
|
|
536
816
|
|
|
537
817
|
// src/linker/helper-package-linker.ts
|
|
@@ -571,7 +851,7 @@ function linkWorkspace(db, workspaceId, vars = {}) {
|
|
|
571
851
|
let edges = linkHelperPackages(db, workspaceId);
|
|
572
852
|
let unresolved = 0;
|
|
573
853
|
const calls = db.prepare(
|
|
574
|
-
`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=?`
|
|
854
|
+
`SELECT c.*,r.name repoName,b.alias,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=?`
|
|
575
855
|
).all(workspaceId);
|
|
576
856
|
for (const call of calls) {
|
|
577
857
|
const callType = String(call.call_type);
|
|
@@ -580,16 +860,38 @@ function linkWorkspace(db, workspaceId, vars = {}) {
|
|
|
580
860
|
call.servicePathExpr ?? call.requireServicePath,
|
|
581
861
|
vars
|
|
582
862
|
);
|
|
583
|
-
const
|
|
863
|
+
const destination = call.destinationExpr ?? call.requireDestination;
|
|
864
|
+
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
865
|
+
const resolution = callType.startsWith("remote") ? resolveOperation(
|
|
866
|
+
db,
|
|
867
|
+
{
|
|
868
|
+
servicePath,
|
|
869
|
+
operationPath: op,
|
|
870
|
+
alias: call.alias,
|
|
871
|
+
destination,
|
|
872
|
+
isDynamic,
|
|
873
|
+
hasExplicitOverride: Object.keys(vars).length > 0
|
|
874
|
+
},
|
|
875
|
+
workspaceId
|
|
876
|
+
) : { status: "unresolved", candidates: [], reasons: [] };
|
|
877
|
+
const target = resolution.target;
|
|
584
878
|
const evidence = {
|
|
585
879
|
sourceFile: call.source_file,
|
|
586
880
|
sourceLine: call.source_line,
|
|
881
|
+
file: call.source_file,
|
|
882
|
+
line: call.source_line,
|
|
883
|
+
repo: call.repoName,
|
|
587
884
|
serviceAlias: call.alias,
|
|
588
|
-
destination
|
|
885
|
+
destination,
|
|
589
886
|
servicePath,
|
|
590
887
|
operationPath: op,
|
|
591
888
|
targetRepo: target?.repoName,
|
|
592
|
-
targetOperation: target?.operationName
|
|
889
|
+
targetOperation: target?.operationName,
|
|
890
|
+
helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
|
|
891
|
+
candidates: resolution.candidates,
|
|
892
|
+
candidateCount: resolution.candidates.length,
|
|
893
|
+
resolutionStatus: resolution.status,
|
|
894
|
+
resolutionReasons: resolution.reasons
|
|
593
895
|
};
|
|
594
896
|
if (target) {
|
|
595
897
|
db.prepare(
|
|
@@ -601,13 +903,13 @@ function linkWorkspace(db, workspaceId, vars = {}) {
|
|
|
601
903
|
String(call.id),
|
|
602
904
|
"operation",
|
|
603
905
|
String(target.operationId),
|
|
604
|
-
|
|
906
|
+
target.score,
|
|
605
907
|
JSON.stringify(evidence),
|
|
606
|
-
|
|
908
|
+
isDynamic ? 1 : 0
|
|
607
909
|
);
|
|
608
910
|
edges += 1;
|
|
609
911
|
} else {
|
|
610
|
-
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" :
|
|
912
|
+
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";
|
|
611
913
|
db.prepare(
|
|
612
914
|
"INSERT INTO graph_edges(workspace_id,edge_type,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?)"
|
|
613
915
|
).run(
|
|
@@ -619,8 +921,10 @@ function linkWorkspace(db, workspaceId, vars = {}) {
|
|
|
619
921
|
String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
|
|
620
922
|
Number(call.confidence ?? 0.2),
|
|
621
923
|
JSON.stringify(evidence),
|
|
622
|
-
|
|
623
|
-
String(
|
|
924
|
+
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
925
|
+
String(
|
|
926
|
+
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")
|
|
927
|
+
)
|
|
624
928
|
);
|
|
625
929
|
edges += 1;
|
|
626
930
|
unresolved += edgeType === "UNRESOLVED_EDGE" ? 1 : 0;
|
|
@@ -641,29 +945,52 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
641
945
|
const handler = start.handler;
|
|
642
946
|
const operation = normalizeOperation(start.operation ?? start.operationPath);
|
|
643
947
|
if (!handler && !operation) return void 0;
|
|
644
|
-
const
|
|
948
|
+
const rows2 = db.prepare(
|
|
949
|
+
`SELECT DISTINCT hc.source_file sourceFile
|
|
645
950
|
FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
646
951
|
WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
|
|
647
|
-
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)`
|
|
648
|
-
|
|
649
|
-
|
|
952
|
+
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)`
|
|
953
|
+
).all(
|
|
954
|
+
repoId,
|
|
955
|
+
repoId,
|
|
956
|
+
handler,
|
|
957
|
+
handler,
|
|
958
|
+
handler,
|
|
959
|
+
operation,
|
|
960
|
+
operation,
|
|
961
|
+
operation
|
|
962
|
+
);
|
|
963
|
+
if (rows2.length === 0) return void 0;
|
|
964
|
+
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
650
965
|
}
|
|
651
966
|
function startScope(db, start) {
|
|
652
|
-
const repo = start.repo ? db.prepare(
|
|
967
|
+
const repo = start.repo ? db.prepare(
|
|
968
|
+
"SELECT id,name FROM repositories WHERE name=? OR package_name=?"
|
|
969
|
+
).get(start.repo, start.repo) : void 0;
|
|
653
970
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
654
971
|
const sourceFiles = sourceFilesForStart(db, repo?.id, start);
|
|
655
|
-
const hasSelector = Boolean(
|
|
656
|
-
|
|
972
|
+
const hasSelector = Boolean(
|
|
973
|
+
start.handler ?? start.operation ?? start.operationPath
|
|
974
|
+
);
|
|
975
|
+
return {
|
|
976
|
+
repo,
|
|
977
|
+
sourceFiles,
|
|
978
|
+
selectorMatched: !hasSelector || sourceFiles !== void 0
|
|
979
|
+
};
|
|
657
980
|
}
|
|
658
981
|
function handlerFilesForOperation(db, operationId) {
|
|
659
|
-
const op = db.prepare(
|
|
660
|
-
|
|
982
|
+
const op = db.prepare(
|
|
983
|
+
`SELECT o.operation_name operationName,o.operation_path operationPath,s.repo_id repoId
|
|
984
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?`
|
|
985
|
+
).get(operationId);
|
|
661
986
|
if (!op) return /* @__PURE__ */ new Set();
|
|
662
987
|
const operation = normalizeOperation(op.operationPath ?? op.operationName);
|
|
663
|
-
const
|
|
988
|
+
const rows2 = db.prepare(
|
|
989
|
+
`SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
|
|
664
990
|
JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
665
|
-
WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
|
|
666
|
-
|
|
991
|
+
WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
|
|
992
|
+
).all(op.repoId, operation, operation, op.operationName);
|
|
993
|
+
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
667
994
|
}
|
|
668
995
|
function includeCall(type, options) {
|
|
669
996
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
@@ -674,48 +1001,145 @@ function includeCall(type, options) {
|
|
|
674
1001
|
function graphForCalls(db, callIds) {
|
|
675
1002
|
const map = /* @__PURE__ */ new Map();
|
|
676
1003
|
if (callIds.length === 0) return map;
|
|
677
|
-
const
|
|
678
|
-
|
|
1004
|
+
const rows2 = db.prepare(
|
|
1005
|
+
`SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
|
|
1006
|
+
).all(...callIds);
|
|
1007
|
+
for (const row of rows2) {
|
|
679
1008
|
const id = Number(row.from_id);
|
|
680
1009
|
map.set(id, [...map.get(id) ?? [], row]);
|
|
681
1010
|
}
|
|
682
1011
|
return map;
|
|
683
1012
|
}
|
|
1013
|
+
function candidatesFromEvidence(evidence) {
|
|
1014
|
+
return Array.isArray(evidence.candidates) ? evidence.candidates.filter(
|
|
1015
|
+
(candidate) => typeof candidate === "object" && candidate !== null
|
|
1016
|
+
) : [];
|
|
1017
|
+
}
|
|
1018
|
+
function evidenceWithRuntimeVariables(evidence, vars) {
|
|
1019
|
+
if (!vars || Object.keys(vars).length === 0) return evidence;
|
|
1020
|
+
const servicePath = typeof evidence.servicePath === "string" ? applyVariables(evidence.servicePath, vars) : void 0;
|
|
1021
|
+
const operationPath = typeof evidence.operationPath === "string" ? applyVariables(evidence.operationPath, vars) : void 0;
|
|
1022
|
+
const candidates = candidatesFromEvidence(evidence);
|
|
1023
|
+
const matched = servicePath ? candidates.find((candidate) => candidate.servicePath === servicePath) : void 0;
|
|
1024
|
+
return {
|
|
1025
|
+
...evidence,
|
|
1026
|
+
servicePath: servicePath ?? evidence.servicePath,
|
|
1027
|
+
operationPath: operationPath ?? evidence.operationPath,
|
|
1028
|
+
runtimeVariablesApplied: true,
|
|
1029
|
+
runtimeResolvedCandidate: matched ? {
|
|
1030
|
+
repoName: matched.repoName,
|
|
1031
|
+
servicePath: matched.servicePath,
|
|
1032
|
+
operationPath: matched.operationPath,
|
|
1033
|
+
operationName: matched.operationName,
|
|
1034
|
+
score: matched.score
|
|
1035
|
+
} : void 0
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function edgeTarget(row, evidence) {
|
|
1039
|
+
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
1040
|
+
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
1041
|
+
return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
1042
|
+
const servicePath = typeof evidence.servicePath === "string" ? evidence.servicePath : void 0;
|
|
1043
|
+
const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
|
|
1044
|
+
const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
|
|
1045
|
+
const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
|
|
1046
|
+
return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
1047
|
+
}
|
|
684
1048
|
function trace(db, start, options) {
|
|
685
1049
|
const scope = startScope(db, start);
|
|
686
|
-
const diagnostics = db.prepare(
|
|
687
|
-
|
|
1050
|
+
const diagnostics = db.prepare(
|
|
1051
|
+
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
|
|
1052
|
+
).all(scope.repo?.id, scope.repo?.id);
|
|
1053
|
+
if (!scope.selectorMatched)
|
|
1054
|
+
diagnostics.unshift({
|
|
1055
|
+
severity: "warning",
|
|
1056
|
+
code: "trace_start_not_found",
|
|
1057
|
+
message: "No handler source matched the requested trace start selector"
|
|
1058
|
+
});
|
|
688
1059
|
const maxDepth = positiveDepth(options.depth);
|
|
689
1060
|
const edges = [];
|
|
690
1061
|
const nodes = /* @__PURE__ */ new Map();
|
|
691
1062
|
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
|
|
692
1063
|
const seenScopes = /* @__PURE__ */ new Set();
|
|
1064
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
693
1065
|
while (queue.length > 0) {
|
|
694
1066
|
const current = queue.shift();
|
|
695
1067
|
if (!current || current.depth > maxDepth) continue;
|
|
696
|
-
const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}
|
|
1068
|
+
const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
|
|
697
1069
|
if (seenScopes.has(key)) continue;
|
|
698
1070
|
seenScopes.add(key);
|
|
699
|
-
const calls = db.prepare(
|
|
700
|
-
|
|
701
|
-
|
|
1071
|
+
const calls = db.prepare(
|
|
1072
|
+
`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`
|
|
1073
|
+
).all(current.repoId, current.repoId);
|
|
1074
|
+
const filtered = calls.filter(
|
|
1075
|
+
(c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
|
|
1076
|
+
);
|
|
1077
|
+
const graph = graphForCalls(
|
|
1078
|
+
db,
|
|
1079
|
+
filtered.map((c) => Number(c.id))
|
|
1080
|
+
);
|
|
702
1081
|
for (const call of filtered) {
|
|
703
1082
|
const callNode = `call:${call.id}`;
|
|
704
|
-
nodes.set(callNode, {
|
|
1083
|
+
nodes.set(callNode, {
|
|
1084
|
+
id: callNode,
|
|
1085
|
+
kind: "outbound_call",
|
|
1086
|
+
repo: call.repoName,
|
|
1087
|
+
file: call.source_file,
|
|
1088
|
+
line: call.source_line,
|
|
1089
|
+
callType: call.call_type
|
|
1090
|
+
});
|
|
705
1091
|
const graphRows = graph.get(Number(call.id)) ?? [];
|
|
706
1092
|
for (const row of graphRows) {
|
|
707
|
-
|
|
1093
|
+
if (seenEdges.has(Number(row.id))) continue;
|
|
1094
|
+
seenEdges.add(Number(row.id));
|
|
1095
|
+
const rawEvidence = JSON.parse(
|
|
1096
|
+
String(row.evidence_json || "{}")
|
|
1097
|
+
);
|
|
1098
|
+
const evidence = evidenceWithRuntimeVariables(
|
|
1099
|
+
rawEvidence,
|
|
1100
|
+
options.vars
|
|
1101
|
+
);
|
|
708
1102
|
const targetNode = `${row.to_kind}:${row.to_id}`;
|
|
709
|
-
nodes.set(targetNode, {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1103
|
+
nodes.set(targetNode, {
|
|
1104
|
+
id: targetNode,
|
|
1105
|
+
kind: row.to_kind,
|
|
1106
|
+
label: row.to_id,
|
|
1107
|
+
...evidence
|
|
1108
|
+
});
|
|
1109
|
+
const to = edgeTarget(row, evidence);
|
|
1110
|
+
edges.push({
|
|
1111
|
+
step: current.depth,
|
|
1112
|
+
type: String(call.call_type),
|
|
1113
|
+
from: `${call.repoName}:${call.source_file}`,
|
|
1114
|
+
to,
|
|
1115
|
+
evidence,
|
|
1116
|
+
confidence: Number(row.confidence ?? call.confidence),
|
|
1117
|
+
unresolvedReason: row.unresolved_reason
|
|
1118
|
+
});
|
|
716
1119
|
if (row.to_kind === "operation" && current.depth < maxDepth) {
|
|
717
1120
|
const files = handlerFilesForOperation(db, row.to_id);
|
|
718
|
-
if (files.size > 0)
|
|
1121
|
+
if (files.size > 0) {
|
|
1122
|
+
const targetRepoId = db.prepare(
|
|
1123
|
+
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1124
|
+
).get(row.to_id)?.repoId;
|
|
1125
|
+
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
1126
|
+
if (seenScopes.has(nextKey))
|
|
1127
|
+
edges.push({
|
|
1128
|
+
step: current.depth + 1,
|
|
1129
|
+
type: "cycle",
|
|
1130
|
+
from: to,
|
|
1131
|
+
to: nextKey,
|
|
1132
|
+
evidence: { ...evidence, cycle: true },
|
|
1133
|
+
confidence: 1,
|
|
1134
|
+
unresolvedReason: "Cycle detected; downstream scope already visited"
|
|
1135
|
+
});
|
|
1136
|
+
else
|
|
1137
|
+
queue.push({
|
|
1138
|
+
repoId: targetRepoId,
|
|
1139
|
+
files,
|
|
1140
|
+
depth: current.depth + 1
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
719
1143
|
}
|
|
720
1144
|
}
|
|
721
1145
|
}
|
|
@@ -739,4 +1163,4 @@ export {
|
|
|
739
1163
|
linkWorkspace,
|
|
740
1164
|
trace
|
|
741
1165
|
};
|
|
742
|
-
//# sourceMappingURL=chunk-
|
|
1166
|
+
//# sourceMappingURL=chunk-SIKIGT42.js.map
|