@saptools/service-flow 0.1.14 → 0.1.16

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/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  parsePackageJson,
11
11
  parseServiceBindings,
12
12
  trace
13
- } from "./chunk-UPXFMLUY.js";
13
+ } from "./chunk-SSSBQIMK.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import { Command } from "commander";
@@ -185,7 +185,7 @@ function migrate(db) {
185
185
  // package.json
186
186
  var package_default = {
187
187
  name: "@saptools/service-flow",
188
- version: "0.1.14",
188
+ version: "0.1.16",
189
189
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
190
190
  type: "module",
191
191
  publishConfig: {
@@ -543,19 +543,44 @@ function insertExecutableSymbols(db, repoId, rows) {
543
543
  for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.kind, r.localName, r.qualifiedName, r.exported ? 1 : 0, r.startLine, r.endLine, r.startOffset, r.endOffset, r.sourceFile, r.exportedName, r.importExportEvidence ? JSON.stringify(r.importExportEvidence) : null);
544
544
  }
545
545
  function insertSymbolCalls(db, repoId, rows) {
546
- const stmt = db.prepare(`INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND ((source_file=? AND (name=? OR qualified_name=?)) OR (source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?))) ORDER BY CASE WHEN source_file=? THEN 0 ELSE 1 END,id LIMIT 1),?,?,?,?,?,0.8,?,CASE WHEN (SELECT id FROM symbols WHERE repo_id=? AND ((source_file=? AND (name=? OR qualified_name=?)) OR (source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?))) ORDER BY CASE WHEN source_file=? THEN 0 ELSE 1 END,id LIMIT 1) IS NULL THEN 'No local symbol target matched exactly' ELSE NULL END)`);
547
- for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.callerQualifiedName, repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, JSON.stringify(r.evidence), repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile);
548
- db.prepare("UPDATE symbol_calls SET status=CASE WHEN callee_symbol_id IS NULL THEN 'unresolved' ELSE 'resolved' END, unresolved_reason=CASE WHEN callee_symbol_id IS NULL THEN COALESCE(unresolved_reason,'No local symbol target matched exactly') ELSE NULL END WHERE repo_id=?").run(repoId);
546
+ const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
547
+ const insertStmt = db.prepare("INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)");
548
+ for (const r of rows) {
549
+ const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
550
+ const target = resolveSymbolCallTarget(db, repoId, r);
551
+ insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
552
+ }
553
+ }
554
+ function resolveSymbolCallTarget(db, repoId, r) {
555
+ const evidence = r.evidence;
556
+ const localRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName);
557
+ if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "same_file_exact", candidateCount: 1 };
558
+ if (localRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: localRows.length };
559
+ const rows = db.prepare("SELECT id,kind,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName);
560
+ if (evidence.relation === "relative_import_proxy_member" && rows.length > 1) {
561
+ const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
562
+ if (objectMapRows.length > 0) {
563
+ const concrete = rows.find((row) => row.kind !== "object_alias") ?? objectMapRows[0];
564
+ return { id: concrete?.id ?? null, status: "resolved", reason: null, strategy: "proxy_member_exported_object_map", candidateCount: rows.length };
565
+ }
566
+ return { id: null, status: "ambiguous", reason: "Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous", strategy: "proxy_member_no_global_name_fallback", candidateCount: rows.length };
567
+ }
568
+ if (rows.length === 1) return { id: rows[0]?.id ?? null, status: "resolved", reason: null, strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_unique_exported_candidate" : "relative_import_exported_exact", candidateCount: 1 };
569
+ if (rows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple exported symbol targets matched exactly", strategy: "exported_exact", candidateCount: rows.length };
570
+ return { id: null, status: "unresolved", reason: "No local symbol target matched exactly", strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_no_global_name_fallback" : "exact_symbol_match", candidateCount: 0 };
549
571
  }
550
572
  function insertCalls(db, repoId, rows) {
551
573
  const stmt = db.prepare(
552
- "INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,service_binding_id) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1),?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))"
574
+ "INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))"
553
575
  );
554
576
  for (const r of rows)
555
577
  stmt.run(
556
578
  repoId,
557
579
  repoId,
558
580
  r.sourceFile,
581
+ r.sourceSymbolQualifiedName,
582
+ repoId,
583
+ r.sourceFile,
559
584
  r.sourceLine,
560
585
  r.sourceLine,
561
586
  r.callType,
@@ -643,6 +668,10 @@ function isFunctionLike(node) {
643
668
  function exported(node) {
644
669
  return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
645
670
  }
671
+ function isPublicStaticMethod(node) {
672
+ const flags = ts.getCombinedModifierFlags(node);
673
+ return (flags & ts.ModifierFlags.Static) !== 0 && (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
674
+ }
646
675
  function exportDeclarations(source) {
647
676
  const exports = /* @__PURE__ */ new Map();
648
677
  const visit = (node) => {
@@ -701,11 +730,20 @@ async function parseExecutableSymbols(repoPath, filePath) {
701
730
  const imports = /* @__PURE__ */ new Map();
702
731
  const exportNames = exportDeclarations(source);
703
732
  const objectExports = /* @__PURE__ */ new Set();
704
- const addSymbol = (kind, localName, node, parentName, exportedName) => {
705
- const declaredExportName = exportedName ?? exportNames.get(parentName ? parentName.split(".")[0] ?? localName : localName);
733
+ const exportedClasses = /* @__PURE__ */ new Set();
734
+ const proxyVariables = /* @__PURE__ */ new Map();
735
+ const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
736
+ const parentRoot = parentName?.split(".")[0] ?? "";
737
+ const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
706
738
  const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
707
- const objectExported = parentName ? objectExports.has(parentName.split(".")[0] ?? "") : false;
708
- symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: objectExported ? qualifiedName : declaredExportName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(declaredExportName) || objectExported, importExportEvidence: declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0 });
739
+ const objectExported = parentName ? objectExports.has(parentRoot) : false;
740
+ const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && isPublicStaticMethod(node) : false;
741
+ const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
742
+ const sourceEvidence = evidence ?? (classMemberExported ? { source: "exported_class_member", exportedClass: parentRoot, memberKind: "static_method" } : declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0);
743
+ symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: effectiveExportedName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(effectiveExportedName), importExportEvidence: sourceEvidence });
744
+ };
745
+ const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
746
+ symbols.push({ kind: "object_alias", localName: propertyName, exportedName: propertyName, qualifiedName: `${objectName}.${propertyName}`, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: true, importExportEvidence: { source: "exported_object_shorthand", objectName, propertyName, targetImportSource } });
709
747
  };
710
748
  const visitImports = (node) => {
711
749
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
@@ -730,12 +768,16 @@ async function parseExecutableSymbols(repoPath, filePath) {
730
768
  visitImports(source);
731
769
  const visitSymbols = (node, parentClass) => {
732
770
  if (ts.isClassDeclaration(node) && node.name) {
771
+ if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
733
772
  for (const member of node.members) visitSymbols(member, node.name.text);
734
773
  return;
735
774
  }
736
775
  if (ts.isMethodDeclaration(node)) {
737
776
  const localName = nameOf(node.name);
738
777
  if (localName) addSymbol("method", localName, node, parentClass);
778
+ } else if (ts.isPropertyDeclaration(node) && parentClass && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
779
+ const localName = nameOf(node.name);
780
+ if (localName) addSymbol("method", localName, node.initializer, parentClass, void 0, { source: "class_property_function", memberKind: ts.isArrowFunction(node.initializer) ? "arrow_function_property" : "function_expression_property" });
739
781
  } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
740
782
  else if (ts.isVariableStatement(node)) {
741
783
  for (const d of node.declarationList.declarations) {
@@ -743,8 +785,10 @@ async function parseExecutableSymbols(repoPath, filePath) {
743
785
  if (!localName || !d.initializer) continue;
744
786
  if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
745
787
  if (ts.isObjectLiteralExpression(d.initializer)) {
746
- if (exported(node) || exportNames.has(localName)) objectExports.add(localName);
788
+ const objectIsExported = exported(node) || exportNames.has(localName);
789
+ if (objectIsExported) objectExports.add(localName);
747
790
  for (const prop of d.initializer.properties) {
791
+ if (objectIsExported && ts.isShorthandPropertyAssignment(prop)) addAliasSymbol(localName, prop.name.text, prop.name, imports.get(prop.name.text));
748
792
  if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
749
793
  const propName = nameOf(prop.name);
750
794
  if (propName) addSymbol("object_method", propName, prop.initializer, localName);
@@ -758,6 +802,45 @@ async function parseExecutableSymbols(repoPath, filePath) {
758
802
  } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
759
803
  };
760
804
  visitSymbols(source);
805
+ const outboundCallNames = /* @__PURE__ */ new Set(["cds.run", "emit", "publish", "on", "send", "axios", "executeHttpRequest", "useOrFetchDestination"]);
806
+ const containsSupportedOutbound = (node) => {
807
+ let found = false;
808
+ const visit = (child) => {
809
+ if (found) return;
810
+ if (ts.isCallExpression(child)) {
811
+ const callee = callName(child.expression);
812
+ if (outboundCallNames.has(callee.expression) || (callee.member ? outboundCallNames.has(callee.member) : false)) found = true;
813
+ }
814
+ ts.forEachChild(child, visit);
815
+ };
816
+ visit(node);
817
+ return found;
818
+ };
819
+ const isTopLevelCallback = (node) => {
820
+ if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
821
+ if (!ts.isCallExpression(node.parent)) return false;
822
+ const callee = callName(node.parent.expression);
823
+ const member = callee.member ?? callee.local;
824
+ return Boolean(member && ["bootstrap", "served", "connect", "on", "once", "use", "get", "post", "put", "patch", "delete", "subscribe"].includes(member));
825
+ };
826
+ const visitCallbackSymbols = (node) => {
827
+ if (isTopLevelCallback(node) && containsSupportedOutbound(node)) {
828
+ const startLine = lineOf(source, node.getStart(source));
829
+ const name = `callback:${startLine}`;
830
+ symbols.push({ kind: "callback", localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: "synthetic_outbound_callback", callbackLine: startLine } });
831
+ }
832
+ ts.forEachChild(node, visitCallbackSymbols);
833
+ };
834
+ visitCallbackSymbols(source);
835
+ const visitProxyVariables = (node) => {
836
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
837
+ const callee = callName(node.initializer.expression);
838
+ const importSource = callee.local ? imports.get(callee.local) : void 0;
839
+ if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression, variableName: node.name.text });
840
+ }
841
+ ts.forEachChild(node, visitProxyVariables);
842
+ };
843
+ visitProxyVariables(source);
761
844
  const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
762
845
  const visitCalls = (node) => {
763
846
  if (ts.isCallExpression(node)) {
@@ -765,8 +848,9 @@ async function parseExecutableSymbols(repoPath, filePath) {
765
848
  const caller = nearest(symbols, line);
766
849
  if (caller) {
767
850
  const callee = callName(node.expression);
768
- const importSource = (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
769
- const targetName = callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
851
+ const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
852
+ const importSource = proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
853
+ const targetName = proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
770
854
  const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
771
855
  const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
772
856
  const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
@@ -778,7 +862,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
778
862
  const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
779
863
  const resolvedTarget = provenThisMethod ? thisTarget : targetName;
780
864
  const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport);
781
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget } });
865
+ if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
782
866
  }
783
867
  }
784
868
  ts.forEachChild(node, visitCalls);
@@ -1047,16 +1131,30 @@ function localServiceDiagnostics(db, strict) {
1047
1131
  }
1048
1132
  function parserQualityDiagnostics(db, strict) {
1049
1133
  if (!strict) return [];
1134
+ const symbolUnresolvedThreshold = 0.05;
1135
+ const dbUnknownThreshold = 0.25;
1136
+ const outboundUnownedThreshold = 0.01;
1050
1137
  const symbol = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) resolved, SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved FROM symbol_calls").get();
1051
1138
  const top = db.prepare("SELECT callee_expression calleeExpression,COUNT(*) count FROM symbol_calls WHERE status='unresolved' GROUP BY callee_expression ORDER BY count DESC,callee_expression LIMIT 5").all();
1139
+ const evidence = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN json_valid(evidence_json)=0 OR json_type(evidence_json) <> 'object' THEN 1 ELSE 0 END) nonObject FROM symbol_calls").get();
1052
1140
  const dbq = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN query_entity IS NOT NULL THEN 1 ELSE 0 END) known, SUM(CASE WHEN query_entity IS NULL THEN 1 ELSE 0 END) unknown FROM outbound_calls WHERE call_type='local_db_query'").get();
1141
+ const outbound = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN source_symbol_id IS NULL THEN 1 ELSE 0 END) withoutOwnership FROM outbound_calls").get();
1142
+ const ownerlessByType = db.prepare("SELECT call_type callType, COUNT(*) count FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY call_type ORDER BY count DESC, call_type").all();
1143
+ const ownerlessByGap = db.prepare("SELECT CASE WHEN source_line <= 1 THEN 'module_preamble' ELSE 'no_indexed_enclosing_symbol' END gap, COUNT(*) count FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY gap ORDER BY count DESC, gap").all();
1053
1144
  const symbolTotal = Number(symbol.total ?? 0);
1054
1145
  const symbolUnresolved = Number(symbol.unresolved ?? 0);
1146
+ const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
1055
1147
  const queryTotal = Number(dbq.total ?? 0);
1056
1148
  const queryUnknown = Number(dbq.unknown ?? 0);
1149
+ const queryUnknownRatio = queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4));
1150
+ const outboundTotal = Number(outbound.total ?? 0);
1151
+ const outboundWithoutOwnership = Number(outbound.withoutOwnership ?? 0);
1152
+ const outboundWithoutOwnershipRatio = outboundTotal === 0 ? 0 : Number((outboundWithoutOwnership / outboundTotal).toFixed(4));
1057
1153
  return [
1058
- { severity: "info", code: "strict_symbol_call_quality", message: "Symbol-call quality aggregate", total: symbolTotal, resolved: Number(symbol.resolved ?? 0), unresolved: symbolUnresolved, unresolvedRatio: symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4)), topUnresolvedCallees: top },
1059
- { severity: "info", code: "strict_db_query_quality", message: "Local DB query quality aggregate", total: queryTotal, known: Number(dbq.known ?? 0), unknown: queryUnknown, unknownRatio: queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4)) }
1154
+ { severity: Number(evidence.nonObject ?? 0) > 0 ? "warning" : "info", code: "strict_symbol_call_evidence_quality", message: "Symbol-call evidence JSON object aggregate", total: Number(evidence.total ?? 0), nonObject: Number(evidence.nonObject ?? 0) },
1155
+ { severity: symbolUnresolvedRatio > symbolUnresolvedThreshold ? "warning" : "info", code: "strict_symbol_call_quality", message: "Symbol-call quality aggregate", total: symbolTotal, resolved: Number(symbol.resolved ?? 0), unresolved: symbolUnresolved, unresolvedRatio: symbolUnresolvedRatio, unresolvedRatioThreshold: symbolUnresolvedThreshold, topUnresolvedCallees: top },
1156
+ { severity: queryUnknownRatio > dbUnknownThreshold ? "warning" : "info", code: "strict_db_query_quality", message: "Local DB query quality aggregate", total: queryTotal, known: Number(dbq.known ?? 0), unknown: queryUnknown, unknownRatio: queryUnknownRatio, unknownRatioThreshold: dbUnknownThreshold },
1157
+ { severity: outboundWithoutOwnershipRatio > outboundUnownedThreshold ? "warning" : "info", code: "strict_outbound_source_ownership_quality", message: "Outbound call source-symbol ownership aggregate", total: outboundTotal, withoutOwnership: outboundWithoutOwnership, withoutOwnershipRatio: outboundWithoutOwnershipRatio, withoutOwnershipRatioThreshold: outboundUnownedThreshold, ownerlessByType, ownerlessByGap }
1060
1158
  ];
1061
1159
  }
1062
1160
  function createProgram() {
@@ -1083,7 +1181,7 @@ function createProgram() {
1083
1181
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
1084
1182
  const r = linkWorkspace(db, workspaceId);
1085
1183
  process.stdout.write(
1086
- `Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} remote ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved
1184
+ `Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved
1087
1185
  `
1088
1186
  );
1089
1187
  }).catch(fail)