@saptools/service-flow 0.1.6 → 0.1.8
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 +11 -0
- package/README.md +26 -2
- package/TECHNICAL-NOTE.md +17 -2
- package/dist/{chunk-6C5HZ6IR.js → chunk-NBAO2R2H.js} +342 -196
- package/dist/chunk-NBAO2R2H.js.map +1 -0
- package/dist/cli.js +159 -146
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-6C5HZ6IR.js.map +0 -1
|
@@ -328,36 +328,176 @@ async function parseDecorators(repoPath, filePath) {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
// src/parsers/handler-registration-parser.ts
|
|
331
|
+
import fsSync from "fs";
|
|
331
332
|
import fs5 from "fs/promises";
|
|
332
333
|
import path6 from "path";
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
import ts3 from "typescript";
|
|
335
|
+
var MAX_EXPORT_DEPTH = 5;
|
|
336
|
+
function lineOf2(sourceFile, node) {
|
|
337
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
338
|
+
}
|
|
339
|
+
function isRelative(source) {
|
|
340
|
+
return source.startsWith("./") || source.startsWith("../");
|
|
341
|
+
}
|
|
342
|
+
function sourceText(node, sourceFile) {
|
|
343
|
+
if (ts3.isIdentifier(node) || ts3.isStringLiteral(node) || ts3.isNumericLiteral(node)) return node.text;
|
|
344
|
+
return node.getText(sourceFile);
|
|
345
|
+
}
|
|
346
|
+
function importSourceFor(identifier, imports) {
|
|
347
|
+
const evidence = imports.get(identifier);
|
|
348
|
+
return evidence ? `${evidence.source}#${evidence.importedName}` : void 0;
|
|
335
349
|
}
|
|
336
350
|
async function parseHandlerRegistrations(repoPath, filePath) {
|
|
337
|
-
const
|
|
351
|
+
const absolutePath = path6.join(repoPath, filePath);
|
|
352
|
+
const text = await fs5.readFile(absolutePath, "utf8");
|
|
353
|
+
const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
|
|
354
|
+
const imports = collectImports(sourceFile);
|
|
355
|
+
const localArrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
|
|
338
356
|
const out = [];
|
|
339
|
-
|
|
340
|
-
|
|
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))
|
|
357
|
+
function emitFromExpression(expression, call) {
|
|
358
|
+
const classes = resolveArrayExpression(expression, localArrays, imports, repoPath, filePath, /* @__PURE__ */ new Set());
|
|
359
|
+
for (const cls of classes) {
|
|
352
360
|
out.push({
|
|
353
|
-
className:
|
|
361
|
+
className: cls.className,
|
|
362
|
+
importSource: cls.importSource,
|
|
354
363
|
registrationFile: normalizePath(filePath),
|
|
355
|
-
registrationLine: lineOf2(
|
|
356
|
-
registrationKind: "handler-
|
|
357
|
-
confidence: 0.
|
|
364
|
+
registrationLine: lineOf2(sourceFile, call),
|
|
365
|
+
registrationKind: "combined-handler-class",
|
|
366
|
+
confidence: 0.95
|
|
358
367
|
});
|
|
368
|
+
}
|
|
369
|
+
if (classes.length === 0) {
|
|
370
|
+
out.push({
|
|
371
|
+
registrationFile: normalizePath(filePath),
|
|
372
|
+
registrationLine: lineOf2(sourceFile, call),
|
|
373
|
+
registrationKind: "combined-handler",
|
|
374
|
+
confidence: 0.75
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function visit(node) {
|
|
379
|
+
if (ts3.isCallExpression(node) && isRegistrationCall(node)) {
|
|
380
|
+
const handlerExpr = handlerExpression(node, sourceFile);
|
|
381
|
+
if (handlerExpr) emitFromExpression(handlerExpr, node);
|
|
382
|
+
else out.push({ registrationFile: normalizePath(filePath), registrationLine: lineOf2(sourceFile, node), registrationKind: "combined-handler", confidence: 0.75 });
|
|
383
|
+
}
|
|
384
|
+
ts3.forEachChild(node, visit);
|
|
385
|
+
}
|
|
386
|
+
visit(sourceFile);
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
function isRegistrationCall(call) {
|
|
390
|
+
const text = call.expression.getText();
|
|
391
|
+
return text.endsWith("createCombinedHandler") || text.endsWith("srv.prepend") || text.endsWith("cds.serve");
|
|
392
|
+
}
|
|
393
|
+
function handlerExpression(call, sourceFile) {
|
|
394
|
+
for (const arg of call.arguments) {
|
|
395
|
+
if (!ts3.isObjectLiteralExpression(arg)) continue;
|
|
396
|
+
for (const prop of arg.properties) {
|
|
397
|
+
if (!ts3.isPropertyAssignment(prop)) continue;
|
|
398
|
+
if (sourceText(prop.name, sourceFile) === "handler") return prop.initializer;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
function collectImports(sourceFile) {
|
|
404
|
+
const imports = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const statement of sourceFile.statements) {
|
|
406
|
+
if (!ts3.isImportDeclaration(statement) || !ts3.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
407
|
+
const source = statement.moduleSpecifier.text;
|
|
408
|
+
const clause = statement.importClause;
|
|
409
|
+
if (!clause) continue;
|
|
410
|
+
if (clause.name) imports.set(clause.name.text, { importedName: "default", source });
|
|
411
|
+
const named = clause.namedBindings;
|
|
412
|
+
if (named && ts3.isNamedImports(named)) {
|
|
413
|
+
for (const element of named.elements) imports.set(element.name.text, { importedName: element.propertyName?.text ?? element.name.text, source });
|
|
414
|
+
}
|
|
415
|
+
if (named && ts3.isNamespaceImport(named)) imports.set(named.name.text, { importedName: "*", source });
|
|
416
|
+
}
|
|
417
|
+
return imports;
|
|
418
|
+
}
|
|
419
|
+
function collectLocalArrays(sourceFile, imports, seed, repoPath = "", fromFile = "") {
|
|
420
|
+
const arrays = new Map(seed);
|
|
421
|
+
for (const statement of sourceFile.statements) {
|
|
422
|
+
if (ts3.isVariableStatement(statement)) {
|
|
423
|
+
for (const decl of statement.declarationList.declarations) {
|
|
424
|
+
if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isArrayLiteralExpression(decl.initializer)) {
|
|
425
|
+
arrays.set(decl.name.text, resolveArrayLiteral(decl.initializer, arrays, imports, repoPath, fromFile, /* @__PURE__ */ new Set()));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return arrays;
|
|
431
|
+
}
|
|
432
|
+
function resolveArrayExpression(expr, arrays, imports, repoPath, fromFile, seen) {
|
|
433
|
+
if (ts3.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen);
|
|
434
|
+
if (ts3.isIdentifier(expr)) {
|
|
435
|
+
const local = arrays.get(expr.text);
|
|
436
|
+
if (local) return local;
|
|
437
|
+
const evidence = imports.get(expr.text);
|
|
438
|
+
if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen);
|
|
439
|
+
if (evidence) return [{ className: evidence.importedName === "default" ? expr.text : evidence.importedName, importSource: `${evidence.source}#${evidence.importedName}` }];
|
|
440
|
+
}
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
function resolveArrayLiteral(array, arrays, imports, repoPath, fromFile, seen) {
|
|
444
|
+
const out = [];
|
|
445
|
+
for (const element of array.elements) {
|
|
446
|
+
if (ts3.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen));
|
|
447
|
+
else if (ts3.isIdentifier(element)) out.push({ className: element.text, importSource: importSourceFor(element.text, imports) });
|
|
448
|
+
}
|
|
359
449
|
return out;
|
|
360
450
|
}
|
|
451
|
+
function resolveImportedArray(repoPath, fromFile, evidence, seen) {
|
|
452
|
+
const moduleFile = resolveRelativeModule(repoPath, fromFile, evidence.source);
|
|
453
|
+
if (!moduleFile) return [];
|
|
454
|
+
const key = `${moduleFile}:${evidence.importedName}`;
|
|
455
|
+
if (seen.has(key) || seen.size > MAX_EXPORT_DEPTH) return [];
|
|
456
|
+
seen.add(key);
|
|
457
|
+
const exports = readExports(repoPath, moduleFile, seen);
|
|
458
|
+
if (evidence.importedName === "default") return exports.defaultArray ?? [];
|
|
459
|
+
return exports.arrays.get(evidence.importedName) ?? exports.arrays.get(exports.aliases.get(evidence.importedName) ?? evidence.importedName) ?? [];
|
|
460
|
+
}
|
|
461
|
+
function resolveRelativeModule(repoPath, fromFile, specifier) {
|
|
462
|
+
const base = path6.resolve(repoPath, path6.dirname(fromFile), specifier);
|
|
463
|
+
for (const candidate of [base, `${base}.ts`, `${base}.js`, path6.join(base, "index.ts"), path6.join(base, "index.js")]) {
|
|
464
|
+
try {
|
|
465
|
+
const stat = fsSync.statSync(candidate);
|
|
466
|
+
if (stat.isFile()) return normalizePath(path6.relative(repoPath, candidate));
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
function readExports(repoPath, filePath, seen) {
|
|
473
|
+
const absolute = path6.join(repoPath, filePath);
|
|
474
|
+
let text;
|
|
475
|
+
try {
|
|
476
|
+
text = fsSync.readFileSync(absolute, "utf8");
|
|
477
|
+
} catch {
|
|
478
|
+
return { arrays: /* @__PURE__ */ new Map(), aliases: /* @__PURE__ */ new Map() };
|
|
479
|
+
}
|
|
480
|
+
const sourceFile = ts3.createSourceFile(filePath, text, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
|
|
481
|
+
const imports = collectImports(sourceFile);
|
|
482
|
+
const arrays = collectLocalArrays(sourceFile, imports, /* @__PURE__ */ new Map(), repoPath, filePath);
|
|
483
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
484
|
+
let defaultArray;
|
|
485
|
+
for (const statement of sourceFile.statements) {
|
|
486
|
+
if (ts3.isExportAssignment(statement) && ts3.isIdentifier(statement.expression)) defaultArray = arrays.get(statement.expression.text);
|
|
487
|
+
if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
|
|
488
|
+
const module = statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : void 0;
|
|
489
|
+
for (const element of statement.exportClause.elements) {
|
|
490
|
+
const local = element.propertyName?.text ?? element.name.text;
|
|
491
|
+
aliases.set(element.name.text, local);
|
|
492
|
+
if (module && isRelative(module)) {
|
|
493
|
+
const imported = resolveImportedArray(repoPath, filePath, { source: module, importedName: local }, seen);
|
|
494
|
+
if (imported.length > 0) arrays.set(element.name.text, imported);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { arrays, defaultArray, aliases };
|
|
500
|
+
}
|
|
361
501
|
|
|
362
502
|
// src/utils/redaction.ts
|
|
363
503
|
var SENSITIVE = /authorization|cookie|token|secret|password|key|credential/i;
|
|
@@ -504,15 +644,15 @@ async function parseOutboundCalls(repoPath, filePath) {
|
|
|
504
644
|
// src/parsers/service-binding-parser.ts
|
|
505
645
|
import fs7 from "fs/promises";
|
|
506
646
|
import path8 from "path";
|
|
507
|
-
import
|
|
647
|
+
import ts4 from "typescript";
|
|
508
648
|
function lineOf4(sf, node) {
|
|
509
649
|
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
510
650
|
}
|
|
511
651
|
function stringValue(node) {
|
|
512
652
|
if (!node) return void 0;
|
|
513
|
-
if (
|
|
653
|
+
if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node))
|
|
514
654
|
return node.text;
|
|
515
|
-
if (
|
|
655
|
+
if (ts4.isTemplateExpression(node))
|
|
516
656
|
return node.getText().replace(/^`|`$/g, "");
|
|
517
657
|
return node.getText();
|
|
518
658
|
}
|
|
@@ -521,22 +661,22 @@ function placeholders(value) {
|
|
|
521
661
|
}
|
|
522
662
|
function connectFactFromCall(call) {
|
|
523
663
|
const expr = call.expression;
|
|
524
|
-
if (!
|
|
664
|
+
if (!ts4.isPropertyAccessExpression(expr) || expr.name.text !== "to")
|
|
525
665
|
return void 0;
|
|
526
666
|
const inner = expr.expression;
|
|
527
|
-
if (!
|
|
667
|
+
if (!ts4.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
|
|
528
668
|
return void 0;
|
|
529
669
|
const first = call.arguments[0];
|
|
530
670
|
if (!first) return void 0;
|
|
531
671
|
const second = call.arguments[1];
|
|
532
|
-
const objectArg =
|
|
672
|
+
const objectArg = ts4.isObjectLiteralExpression(first) ? first : second && ts4.isObjectLiteralExpression(second) ? second : void 0;
|
|
533
673
|
let alias;
|
|
534
674
|
let aliasExpr;
|
|
535
|
-
if (
|
|
675
|
+
if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first))
|
|
536
676
|
alias = first.text;
|
|
537
|
-
else if (!
|
|
677
|
+
else if (!ts4.isObjectLiteralExpression(first))
|
|
538
678
|
aliasExpr = stringValue(first);
|
|
539
|
-
if ((
|
|
679
|
+
if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
|
|
540
680
|
return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
541
681
|
if (!objectArg && aliasExpr)
|
|
542
682
|
return {
|
|
@@ -548,13 +688,13 @@ function connectFactFromCall(call) {
|
|
|
548
688
|
let servicePathExpr;
|
|
549
689
|
function visitObject(obj) {
|
|
550
690
|
for (const prop of obj.properties) {
|
|
551
|
-
if (!
|
|
552
|
-
const name =
|
|
691
|
+
if (!ts4.isPropertyAssignment(prop)) continue;
|
|
692
|
+
const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
553
693
|
if (name === "destination")
|
|
554
694
|
destinationExpr = stringValue(prop.initializer);
|
|
555
695
|
if (name === "path" || name === "servicePath")
|
|
556
696
|
servicePathExpr = stringValue(prop.initializer);
|
|
557
|
-
if (
|
|
697
|
+
if (ts4.isObjectLiteralExpression(prop.initializer))
|
|
558
698
|
visitObject(prop.initializer);
|
|
559
699
|
}
|
|
560
700
|
}
|
|
@@ -574,8 +714,8 @@ function connectFactFromCall(call) {
|
|
|
574
714
|
};
|
|
575
715
|
}
|
|
576
716
|
function unwrapCall(expr) {
|
|
577
|
-
if (
|
|
578
|
-
if (
|
|
717
|
+
if (ts4.isAwaitExpression(expr)) return unwrapCall(expr.expression);
|
|
718
|
+
if (ts4.isCallExpression(expr)) return expr;
|
|
579
719
|
return void 0;
|
|
580
720
|
}
|
|
581
721
|
function findConnectInExpression(expr) {
|
|
@@ -587,8 +727,8 @@ function findConnectInExpression(expr) {
|
|
|
587
727
|
let found;
|
|
588
728
|
function visit(node) {
|
|
589
729
|
if (found) return;
|
|
590
|
-
if (
|
|
591
|
-
if (!found)
|
|
730
|
+
if (ts4.isCallExpression(node)) found = connectFactFromCall(node);
|
|
731
|
+
if (!found) ts4.forEachChild(node, visit);
|
|
592
732
|
}
|
|
593
733
|
visit(expr);
|
|
594
734
|
return found;
|
|
@@ -596,12 +736,12 @@ function findConnectInExpression(expr) {
|
|
|
596
736
|
async function readSource(abs) {
|
|
597
737
|
try {
|
|
598
738
|
const text = await fs7.readFile(abs, "utf8");
|
|
599
|
-
return
|
|
739
|
+
return ts4.createSourceFile(
|
|
600
740
|
abs,
|
|
601
741
|
text,
|
|
602
|
-
|
|
742
|
+
ts4.ScriptTarget.Latest,
|
|
603
743
|
true,
|
|
604
|
-
|
|
744
|
+
ts4.ScriptKind.TS
|
|
605
745
|
);
|
|
606
746
|
} catch {
|
|
607
747
|
return void 0;
|
|
@@ -630,7 +770,7 @@ async function resolveImport(repoPath, fromFile, spec) {
|
|
|
630
770
|
async function importsFor(repoPath, filePath, sf) {
|
|
631
771
|
const imports = [];
|
|
632
772
|
for (const stmt of sf.statements) {
|
|
633
|
-
if (!
|
|
773
|
+
if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteralLike(stmt.moduleSpecifier))
|
|
634
774
|
continue;
|
|
635
775
|
const sourceFile = await resolveImport(
|
|
636
776
|
repoPath,
|
|
@@ -646,7 +786,7 @@ async function importsFor(repoPath, filePath, sf) {
|
|
|
646
786
|
sourceFile
|
|
647
787
|
});
|
|
648
788
|
const bindings = clause.namedBindings;
|
|
649
|
-
if (bindings &&
|
|
789
|
+
if (bindings && ts4.isNamedImports(bindings))
|
|
650
790
|
for (const el of bindings.elements)
|
|
651
791
|
imports.push({
|
|
652
792
|
localName: el.name.text,
|
|
@@ -659,17 +799,17 @@ async function importsFor(repoPath, filePath, sf) {
|
|
|
659
799
|
function exportedLocalNames(sf) {
|
|
660
800
|
const exports = /* @__PURE__ */ new Map();
|
|
661
801
|
for (const stmt of sf.statements) {
|
|
662
|
-
const direct =
|
|
663
|
-
(m) => m.kind ===
|
|
802
|
+
const direct = ts4.canHaveModifiers(stmt) ? ts4.getModifiers(stmt)?.some(
|
|
803
|
+
(m) => m.kind === ts4.SyntaxKind.ExportKeyword
|
|
664
804
|
) ?? false : false;
|
|
665
|
-
if (direct &&
|
|
805
|
+
if (direct && ts4.isFunctionDeclaration(stmt) && stmt.name)
|
|
666
806
|
exports.set(stmt.name.text, stmt.name.text);
|
|
667
|
-
if (direct &&
|
|
807
|
+
if (direct && ts4.isVariableStatement(stmt)) {
|
|
668
808
|
for (const decl of stmt.declarationList.declarations)
|
|
669
|
-
if (
|
|
809
|
+
if (ts4.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
|
|
670
810
|
}
|
|
671
|
-
if (!
|
|
672
|
-
if (!
|
|
811
|
+
if (!ts4.isExportDeclaration(stmt) || !stmt.exportClause) continue;
|
|
812
|
+
if (!ts4.isNamedExports(stmt.exportClause)) continue;
|
|
673
813
|
for (const el of stmt.exportClause.elements)
|
|
674
814
|
exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
|
|
675
815
|
}
|
|
@@ -683,18 +823,18 @@ async function helperBindings(repoPath, filePath) {
|
|
|
683
823
|
const exportedLocals = exportedLocalNames(sf);
|
|
684
824
|
const factsByLocal = /* @__PURE__ */ new Map();
|
|
685
825
|
for (const stmt of sf.statements) {
|
|
686
|
-
if (
|
|
826
|
+
if (ts4.isFunctionDeclaration(stmt) && stmt.name) {
|
|
687
827
|
let fact;
|
|
688
828
|
stmt.forEachChild(function visit(node) {
|
|
689
|
-
if (!fact &&
|
|
829
|
+
if (!fact && ts4.isReturnStatement(node) && node.expression)
|
|
690
830
|
fact = findConnectInExpression(node.expression);
|
|
691
|
-
if (!fact)
|
|
831
|
+
if (!fact) ts4.forEachChild(node, visit);
|
|
692
832
|
});
|
|
693
833
|
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
|
|
694
834
|
}
|
|
695
|
-
if (
|
|
835
|
+
if (ts4.isVariableStatement(stmt))
|
|
696
836
|
for (const decl of stmt.declarationList.declarations) {
|
|
697
|
-
if (!
|
|
837
|
+
if (!ts4.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
698
838
|
const fact = findConnectInExpression(decl.initializer);
|
|
699
839
|
if (fact)
|
|
700
840
|
factsByLocal.set(decl.name.text, {
|
|
@@ -735,7 +875,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
735
875
|
return helper ? { imp, helper } : void 0;
|
|
736
876
|
}
|
|
737
877
|
async function recordVariable(decl) {
|
|
738
|
-
if (!
|
|
878
|
+
if (!ts4.isIdentifier(decl.name) || !decl.initializer) return;
|
|
739
879
|
const call = unwrapCall(decl.initializer);
|
|
740
880
|
if (!call) return;
|
|
741
881
|
const direct = connectFactFromCall(call);
|
|
@@ -746,7 +886,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
746
886
|
sourceFile: normalizePath(filePath),
|
|
747
887
|
sourceLine: lineOf4(sourceFileAst, decl)
|
|
748
888
|
});
|
|
749
|
-
else if (
|
|
889
|
+
else if (ts4.isIdentifier(call.expression)) {
|
|
750
890
|
const resolved = await importedHelper(call.expression.text);
|
|
751
891
|
if (resolved)
|
|
752
892
|
out.push({
|
|
@@ -773,14 +913,14 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
773
913
|
}
|
|
774
914
|
}
|
|
775
915
|
function recordDestructuredClassHelper(decl) {
|
|
776
|
-
if (!
|
|
916
|
+
if (!ts4.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
777
917
|
const call = unwrapCall(decl.initializer);
|
|
778
|
-
if (!call || !
|
|
918
|
+
if (!call || !ts4.isPropertyAccessExpression(call.expression)) return;
|
|
779
919
|
const target = call.expression;
|
|
780
|
-
if (target.expression.kind !==
|
|
920
|
+
if (target.expression.kind !== ts4.SyntaxKind.ThisKeyword) return;
|
|
781
921
|
for (const el of decl.name.elements) {
|
|
782
|
-
if (!
|
|
783
|
-
const propertyName = el.propertyName &&
|
|
922
|
+
if (!ts4.isIdentifier(el.name)) continue;
|
|
923
|
+
const propertyName = el.propertyName && ts4.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
784
924
|
const helper = classHelpers.find(
|
|
785
925
|
(h) => h.helperName === target.name.text && h.propertyName === propertyName
|
|
786
926
|
);
|
|
@@ -806,8 +946,8 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
806
946
|
}
|
|
807
947
|
const declarations = [];
|
|
808
948
|
function collectDeclarations(node) {
|
|
809
|
-
if (
|
|
810
|
-
|
|
949
|
+
if (ts4.isVariableDeclaration(node)) declarations.push(node);
|
|
950
|
+
ts4.forEachChild(node, collectDeclarations);
|
|
811
951
|
}
|
|
812
952
|
collectDeclarations(sourceFileAst);
|
|
813
953
|
for (const decl of declarations) {
|
|
@@ -819,16 +959,16 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
819
959
|
function collectClassHelpers(sf) {
|
|
820
960
|
const helpers = [];
|
|
821
961
|
for (const stmt of sf.statements) {
|
|
822
|
-
if (!
|
|
962
|
+
if (!ts4.isClassDeclaration(stmt) || !stmt.name) continue;
|
|
823
963
|
for (const member of stmt.members) {
|
|
824
964
|
let visit2 = function(node) {
|
|
825
|
-
if (
|
|
965
|
+
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
|
|
826
966
|
const fact = findConnectInExpression(node.initializer);
|
|
827
967
|
if (fact) bindings.set(node.name.text, fact);
|
|
828
968
|
}
|
|
829
|
-
if (
|
|
969
|
+
if (ts4.isReturnStatement(node) && node.expression && ts4.isObjectLiteralExpression(node.expression)) {
|
|
830
970
|
for (const prop of node.expression.properties) {
|
|
831
|
-
if (
|
|
971
|
+
if (ts4.isShorthandPropertyAssignment(prop)) {
|
|
832
972
|
const fact = bindings.get(prop.name.text);
|
|
833
973
|
if (fact)
|
|
834
974
|
helpers.push({
|
|
@@ -840,8 +980,8 @@ function collectClassHelpers(sf) {
|
|
|
840
980
|
sourceLine: lineOf4(sf, prop)
|
|
841
981
|
});
|
|
842
982
|
}
|
|
843
|
-
if (
|
|
844
|
-
const propertyName =
|
|
983
|
+
if (ts4.isPropertyAssignment(prop) && ts4.isIdentifier(prop.initializer)) {
|
|
984
|
+
const propertyName = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
845
985
|
const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
|
|
846
986
|
if (propertyName && fact)
|
|
847
987
|
helpers.push({
|
|
@@ -855,15 +995,15 @@ function collectClassHelpers(sf) {
|
|
|
855
995
|
}
|
|
856
996
|
}
|
|
857
997
|
}
|
|
858
|
-
|
|
998
|
+
ts4.forEachChild(node, visit2);
|
|
859
999
|
};
|
|
860
1000
|
var visit = visit2;
|
|
861
|
-
if (!
|
|
862
|
-
if (!
|
|
1001
|
+
if (!ts4.isPropertyDeclaration(member) || !member.initializer) continue;
|
|
1002
|
+
if (!ts4.isIdentifier(member.name)) continue;
|
|
863
1003
|
const className = stmt.name.text;
|
|
864
1004
|
const helperName = member.name.text;
|
|
865
1005
|
const initializer = member.initializer;
|
|
866
|
-
if (!
|
|
1006
|
+
if (!ts4.isArrowFunction(initializer) && !ts4.isFunctionExpression(initializer))
|
|
867
1007
|
continue;
|
|
868
1008
|
const bindings = /* @__PURE__ */ new Map();
|
|
869
1009
|
visit2(initializer);
|
|
@@ -992,149 +1132,143 @@ function normalizeName(value) {
|
|
|
992
1132
|
}
|
|
993
1133
|
function candidatesForDependency(repos, dep, sourceId) {
|
|
994
1134
|
const exact = repos.filter((repo) => repo.id !== sourceId && repo.package_name === dep);
|
|
995
|
-
if (exact.length > 0) return exact;
|
|
1135
|
+
if (exact.length > 0) return { candidates: exact, strategy: "exact_package_name" };
|
|
996
1136
|
const normalized = normalizeName(dep);
|
|
997
|
-
return repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized);
|
|
1137
|
+
return { candidates: repos.filter((repo) => repo.id !== sourceId && normalizeName(repo.name) === normalized), strategy: "normalized_directory" };
|
|
998
1138
|
}
|
|
999
|
-
function linkHelperPackages(db, workspaceId) {
|
|
1000
|
-
const repos = db.prepare(
|
|
1001
|
-
|
|
1002
|
-
).all(workspaceId);
|
|
1003
|
-
let count = 0;
|
|
1139
|
+
function linkHelperPackages(db, workspaceId, generation) {
|
|
1140
|
+
const repos = db.prepare("SELECT id,name,package_name,dependencies_json FROM repositories WHERE workspace_id=?").all(workspaceId);
|
|
1141
|
+
const summary = { edgeCount: 0, resolvedCount: 0, ambiguousCount: 0 };
|
|
1004
1142
|
for (const repo of repos) {
|
|
1005
1143
|
const deps = JSON.parse(repo.dependencies_json);
|
|
1006
1144
|
for (const dep of Object.keys(deps)) {
|
|
1007
|
-
const
|
|
1008
|
-
if (candidates.length === 0) continue;
|
|
1009
|
-
const status = candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1010
|
-
const helper =
|
|
1011
|
-
db.prepare(
|
|
1012
|
-
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
|
1013
|
-
).run(
|
|
1145
|
+
const result = candidatesForDependency(repos, dep, repo.id);
|
|
1146
|
+
if (result.candidates.length === 0) continue;
|
|
1147
|
+
const status = result.candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1148
|
+
const helper = status === "resolved" ? result.candidates[0] : void 0;
|
|
1149
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(
|
|
1014
1150
|
workspaceId,
|
|
1015
1151
|
"REPO_IMPORTS_HELPER_PACKAGE",
|
|
1016
1152
|
status,
|
|
1017
1153
|
"repo",
|
|
1018
1154
|
String(repo.id),
|
|
1019
1155
|
helper ? "repo" : "repo_candidates",
|
|
1020
|
-
helper ? String(helper.id) : candidates.map((candidate) => candidate.id).join(","),
|
|
1156
|
+
helper ? String(helper.id) : result.candidates.map((candidate) => candidate.id).join(","),
|
|
1021
1157
|
helper ? 1 : 0.5,
|
|
1022
|
-
JSON.stringify({
|
|
1023
|
-
dependency: dep,
|
|
1024
|
-
candidates: candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })),
|
|
1025
|
-
match: helper?.package_name === dep ? "package_name" : "normalized_directory"
|
|
1026
|
-
}),
|
|
1158
|
+
JSON.stringify({ dependency: dep, candidates: result.candidates.map((candidate) => ({ id: candidate.id, name: candidate.name, packageName: candidate.package_name })), match: result.strategy }),
|
|
1027
1159
|
0,
|
|
1028
|
-
helper ? null : "Ambiguous dependency package candidates"
|
|
1160
|
+
helper ? null : "Ambiguous dependency package candidates",
|
|
1161
|
+
generation
|
|
1029
1162
|
);
|
|
1030
|
-
|
|
1163
|
+
summary.edgeCount += 1;
|
|
1164
|
+
if (helper) summary.resolvedCount += 1;
|
|
1165
|
+
else summary.ambiguousCount += 1;
|
|
1031
1166
|
}
|
|
1032
1167
|
}
|
|
1033
|
-
return
|
|
1168
|
+
return summary;
|
|
1034
1169
|
}
|
|
1035
1170
|
|
|
1036
1171
|
// src/linker/cross-repo-linker.ts
|
|
1037
1172
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
1038
1173
|
return db.transaction(() => {
|
|
1174
|
+
const generation = nextGraphGeneration(db, workspaceId);
|
|
1039
1175
|
db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
let terminalCount = 0;
|
|
1046
|
-
const calls = db.prepare(
|
|
1047
|
-
`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=?`
|
|
1048
|
-
).all(workspaceId);
|
|
1049
|
-
for (const call of calls) {
|
|
1050
|
-
const callType = String(call.call_type);
|
|
1051
|
-
const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1052
|
-
const servicePath = applyVariables(
|
|
1053
|
-
call.servicePathExpr ?? call.requireServicePath,
|
|
1054
|
-
vars
|
|
1055
|
-
);
|
|
1056
|
-
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1057
|
-
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1058
|
-
const resolution = callType.startsWith("remote") ? resolveOperation(
|
|
1059
|
-
db,
|
|
1060
|
-
{
|
|
1061
|
-
servicePath,
|
|
1062
|
-
operationPath: op,
|
|
1063
|
-
alias: applyVariables(call.aliasExpr ?? call.alias, vars),
|
|
1064
|
-
destination: applyVariables(destination, vars),
|
|
1065
|
-
isDynamic,
|
|
1066
|
-
hasExplicitOverride: Object.keys(vars).length > 0
|
|
1067
|
-
},
|
|
1068
|
-
workspaceId
|
|
1069
|
-
) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1070
|
-
const target = resolution.target;
|
|
1071
|
-
const evidence = {
|
|
1072
|
-
sourceFile: call.source_file,
|
|
1073
|
-
sourceLine: call.source_line,
|
|
1074
|
-
file: call.source_file,
|
|
1075
|
-
line: call.source_line,
|
|
1076
|
-
repo: call.repoName,
|
|
1077
|
-
serviceAlias: call.alias,
|
|
1078
|
-
serviceAliasExpr: call.aliasExpr,
|
|
1079
|
-
destination: applyVariables(destination, vars),
|
|
1080
|
-
servicePath,
|
|
1081
|
-
operationPath: op,
|
|
1082
|
-
targetRepo: target?.repoName,
|
|
1083
|
-
targetOperation: target?.operationName,
|
|
1084
|
-
helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
|
|
1085
|
-
candidates: resolution.candidates,
|
|
1086
|
-
candidateCount: resolution.candidates.length,
|
|
1087
|
-
resolutionStatus: resolution.status,
|
|
1088
|
-
resolutionReasons: resolution.reasons
|
|
1089
|
-
};
|
|
1090
|
-
if (target) {
|
|
1091
|
-
db.prepare(
|
|
1092
|
-
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?,?)"
|
|
1093
|
-
).run(
|
|
1094
|
-
workspaceId,
|
|
1095
|
-
"REMOTE_CALL_RESOLVES_TO_OPERATION",
|
|
1096
|
-
"resolved",
|
|
1097
|
-
"call",
|
|
1098
|
-
String(call.id),
|
|
1099
|
-
"operation",
|
|
1100
|
-
String(target.operationId),
|
|
1101
|
-
target.score,
|
|
1102
|
-
JSON.stringify(evidence),
|
|
1103
|
-
isDynamic ? 1 : 0
|
|
1104
|
-
);
|
|
1105
|
-
edges += 1;
|
|
1106
|
-
resolvedCount += 1;
|
|
1107
|
-
} else {
|
|
1108
|
-
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";
|
|
1109
|
-
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1110
|
-
const unresolvedReason = status === "terminal" ? null : String(
|
|
1111
|
-
call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched")
|
|
1112
|
-
);
|
|
1113
|
-
db.prepare(
|
|
1114
|
-
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
|
1115
|
-
).run(
|
|
1116
|
-
workspaceId,
|
|
1117
|
-
edgeType,
|
|
1118
|
-
status,
|
|
1119
|
-
"call",
|
|
1120
|
-
String(call.id),
|
|
1121
|
-
callType.startsWith("async_") ? "event" : "external",
|
|
1122
|
-
String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
|
|
1123
|
-
Number(call.confidence ?? 0.2),
|
|
1124
|
-
JSON.stringify(evidence),
|
|
1125
|
-
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
1126
|
-
unresolvedReason
|
|
1127
|
-
);
|
|
1128
|
-
edges += 1;
|
|
1129
|
-
unresolved += status === "unresolved" ? 1 : 0;
|
|
1130
|
-
ambiguousCount += status === "ambiguous" ? 1 : 0;
|
|
1131
|
-
dynamicCount += status === "dynamic" ? 1 : 0;
|
|
1132
|
-
terminalCount += status === "terminal" ? 1 : 0;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
return { edgeCount: edges, unresolvedCount: unresolved, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1176
|
+
const deps = linkHelperPackages(db, workspaceId, generation);
|
|
1177
|
+
const callSummary = linkCalls(db, workspaceId, vars, generation);
|
|
1178
|
+
const impl = linkImplementations(db, workspaceId, generation);
|
|
1179
|
+
db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
|
|
1180
|
+
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount };
|
|
1136
1181
|
});
|
|
1137
1182
|
}
|
|
1183
|
+
function nextGraphGeneration(db, workspaceId) {
|
|
1184
|
+
const row = db.prepare("SELECT COALESCE(MAX(graph_generation),0) generation FROM repositories WHERE workspace_id=?").get(workspaceId);
|
|
1185
|
+
return Number(row?.generation ?? 0) + 1;
|
|
1186
|
+
}
|
|
1187
|
+
function linkCalls(db, workspaceId, vars, generation) {
|
|
1188
|
+
let edgeCount = 0;
|
|
1189
|
+
let unresolvedCount = 0;
|
|
1190
|
+
let resolvedCount = 0;
|
|
1191
|
+
let ambiguousCount = 0;
|
|
1192
|
+
let dynamicCount = 0;
|
|
1193
|
+
let terminalCount = 0;
|
|
1194
|
+
const calls = db.prepare(`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=?`).all(workspaceId);
|
|
1195
|
+
for (const call of calls) {
|
|
1196
|
+
const result = insertCallEdge(db, workspaceId, call, vars, generation);
|
|
1197
|
+
edgeCount += 1;
|
|
1198
|
+
resolvedCount += result.status === "resolved" ? 1 : 0;
|
|
1199
|
+
unresolvedCount += result.status === "unresolved" ? 1 : 0;
|
|
1200
|
+
ambiguousCount += result.status === "ambiguous" ? 1 : 0;
|
|
1201
|
+
dynamicCount += result.status === "dynamic" ? 1 : 0;
|
|
1202
|
+
terminalCount += result.status === "terminal" ? 1 : 0;
|
|
1203
|
+
}
|
|
1204
|
+
return { edgeCount, unresolvedCount, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1205
|
+
}
|
|
1206
|
+
function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
1207
|
+
const callType = String(call.call_type);
|
|
1208
|
+
const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1209
|
+
const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
|
|
1210
|
+
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1211
|
+
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1212
|
+
const resolution = callType.startsWith("remote") ? resolveOperation(db, { servicePath, operationPath: op, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1213
|
+
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
|
|
1214
|
+
if (resolution.target) {
|
|
1215
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
|
|
1216
|
+
return { status: "resolved" };
|
|
1217
|
+
}
|
|
1218
|
+
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";
|
|
1219
|
+
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1220
|
+
const unresolvedReason = status === "terminal" ? null : String(call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched"));
|
|
1221
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, edgeType, status, "call", String(call.id), callType.startsWith("async_") ? "event" : "external", String(call.event_name_expr ?? call.query_entity ?? op ?? call.id), Number(call.confidence ?? 0.2), JSON.stringify(evidence), isDynamic || resolution.status === "dynamic" ? 1 : 0, unresolvedReason, generation);
|
|
1222
|
+
return { status };
|
|
1223
|
+
}
|
|
1224
|
+
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1225
|
+
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1226
|
+
}
|
|
1227
|
+
function linkImplementations(db, workspaceId, generation) {
|
|
1228
|
+
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.package_name modelPackage FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
|
|
1229
|
+
let edgeCount = 0;
|
|
1230
|
+
let resolvedCount = 0;
|
|
1231
|
+
let ambiguousCount = 0;
|
|
1232
|
+
for (const operation of operations) {
|
|
1233
|
+
const rows2 = implementationCandidates(db, workspaceId, operation);
|
|
1234
|
+
if (rows2.length === 0) continue;
|
|
1235
|
+
const unique = rows2.length === 1 ? rows2[0] : void 0;
|
|
1236
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique ? "resolved" : "ambiguous", "operation", String(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? String(unique.methodId) : rows2.map((row) => row.methodId).join(","), unique ? 0.95 : 0.5, JSON.stringify({ servicePath: operation.servicePath, operationPath: operation.operationPath, operationName: operation.operationName, candidates: rows2, evidence: "registered_application_dependency" }), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
|
|
1237
|
+
edgeCount += 1;
|
|
1238
|
+
if (unique) resolvedCount += 1;
|
|
1239
|
+
else ambiguousCount += 1;
|
|
1240
|
+
}
|
|
1241
|
+
return { edgeCount, resolvedCount, ambiguousCount };
|
|
1242
|
+
}
|
|
1243
|
+
function implementationCandidates(db, workspaceId, operation) {
|
|
1244
|
+
const rows2 = db.prepare(`SELECT DISTINCT
|
|
1245
|
+
hm.id methodId,
|
|
1246
|
+
hc.id classId,
|
|
1247
|
+
hc.class_name className,
|
|
1248
|
+
hc.source_file sourceFile,
|
|
1249
|
+
hc.source_line sourceLine,
|
|
1250
|
+
hr.repo_id applicationRepoId,
|
|
1251
|
+
hr.registration_file registrationFile,
|
|
1252
|
+
hr.registration_line registrationLine,
|
|
1253
|
+
hr.import_source importSource,
|
|
1254
|
+
handlerRepo.id handlerRepoId,
|
|
1255
|
+
handlerRepo.name handlerRepo,
|
|
1256
|
+
appRepo.name applicationRepo,
|
|
1257
|
+
CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
|
|
1258
|
+
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(? AS TEXT)) THEN 1 ELSE 0 END appDependsOnModel,
|
|
1259
|
+
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=CAST(? AS TEXT)) THEN 1 ELSE 0 END handlerDependsOnModel
|
|
1260
|
+
FROM handler_methods hm
|
|
1261
|
+
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
1262
|
+
JOIN repositories handlerRepo ON handlerRepo.id=hc.repo_id
|
|
1263
|
+
JOIN handler_registrations hr ON (hr.handler_class_id=hc.id OR (hr.class_name=hc.class_name AND (hr.repo_id=hc.repo_id OR hr.import_source IS NOT NULL)))
|
|
1264
|
+
JOIN repositories appRepo ON appRepo.id=hr.repo_id
|
|
1265
|
+
WHERE appRepo.workspace_id=?
|
|
1266
|
+
AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(operation.modelRepoId, operation.modelRepoId, workspaceId, normalizedOperation(String(operation.operationPath ?? "")), operation.operationName, operation.operationName);
|
|
1267
|
+
return rows2.filter((row) => Boolean(Number(row.sameRepoRegistration)) || Boolean(Number(row.appDependsOnModel)) || Boolean(Number(row.handlerDependsOnModel)));
|
|
1268
|
+
}
|
|
1269
|
+
function normalizedOperation(value) {
|
|
1270
|
+
return value.startsWith("/") ? value.slice(1) : value;
|
|
1271
|
+
}
|
|
1138
1272
|
|
|
1139
1273
|
// src/trace/trace-engine.ts
|
|
1140
1274
|
function normalizeOperation(value) {
|
|
@@ -1182,8 +1316,10 @@ function startScope(db, start) {
|
|
|
1182
1316
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
1183
1317
|
const sourceFiles = sourceFilesForStart(db, repo?.id, start);
|
|
1184
1318
|
const hasSelector = Boolean(
|
|
1185
|
-
start.handler ?? start.operation ?? start.operationPath
|
|
1319
|
+
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
|
|
1186
1320
|
);
|
|
1321
|
+
if (start.servicePath && !start.operation && !start.operationPath && !start.handler)
|
|
1322
|
+
return { repo, selectorMatched: false };
|
|
1187
1323
|
return {
|
|
1188
1324
|
repo,
|
|
1189
1325
|
sourceFiles,
|
|
@@ -1204,6 +1340,12 @@ function handlerFilesForOperation(db, operationId) {
|
|
|
1204
1340
|
).all(op.repoId, operation, operation, op.operationName);
|
|
1205
1341
|
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
1206
1342
|
}
|
|
1343
|
+
function implementationScope(db, operationId) {
|
|
1344
|
+
const edge = db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? ORDER BY id LIMIT 1").get(operationId);
|
|
1345
|
+
if (!edge) return { files: /* @__PURE__ */ new Set() };
|
|
1346
|
+
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
|
|
1347
|
+
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []) };
|
|
1348
|
+
}
|
|
1207
1349
|
function includeCall(type, options) {
|
|
1208
1350
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
1209
1351
|
if (!options.includeExternal && type === "external_http") return false;
|
|
@@ -1288,11 +1430,14 @@ function trace(db, start, options) {
|
|
|
1288
1430
|
const diagnostics = db.prepare(
|
|
1289
1431
|
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)"
|
|
1290
1432
|
).all(scope.repo?.id, scope.repo?.id);
|
|
1433
|
+
const stale = db.prepare("SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?)").all(scope.repo?.id, scope.repo?.id);
|
|
1434
|
+
for (const row of stale)
|
|
1435
|
+
diagnostics.unshift({ severity: "warning", code: "graph_stale", message: `Graph is stale for ${row.name ?? "repository"}: ${row.reason ?? "facts_changed"}. Run service-flow link.` });
|
|
1291
1436
|
if (!scope.selectorMatched)
|
|
1292
1437
|
diagnostics.unshift({
|
|
1293
1438
|
severity: "warning",
|
|
1294
1439
|
code: "trace_start_not_found",
|
|
1295
|
-
message: "No handler source matched the requested trace start selector"
|
|
1440
|
+
message: start.servicePath && !start.operation && !start.operationPath && !start.handler ? "Service-only trace requires --operation or --path and will not broaden to the whole workspace" : "No handler source matched the requested trace start selector"
|
|
1296
1441
|
});
|
|
1297
1442
|
const maxDepth = positiveDepth(options.depth);
|
|
1298
1443
|
const edges = [];
|
|
@@ -1354,9 +1499,10 @@ function trace(db, start, options) {
|
|
|
1354
1499
|
unresolvedReason: effective.unresolvedReason
|
|
1355
1500
|
});
|
|
1356
1501
|
if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
|
|
1357
|
-
const
|
|
1502
|
+
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
1503
|
+
const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
|
|
1358
1504
|
if (files.size > 0) {
|
|
1359
|
-
const targetRepoId = db.prepare(
|
|
1505
|
+
const targetRepoId = implementation.repoId ?? db.prepare(
|
|
1360
1506
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1361
1507
|
).get(effectiveRow.to_id)?.repoId;
|
|
1362
1508
|
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
@@ -1402,4 +1548,4 @@ export {
|
|
|
1402
1548
|
linkWorkspace,
|
|
1403
1549
|
trace
|
|
1404
1550
|
};
|
|
1405
|
-
//# sourceMappingURL=chunk-
|
|
1551
|
+
//# sourceMappingURL=chunk-NBAO2R2H.js.map
|