@saptools/service-flow 0.1.13 → 0.1.15

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-Q7W7TE3W.js";
13
+ } from "./chunk-ZSUL2X77.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.13",
188
+ version: "0.1.15",
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,9 +543,25 @@ 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 findTarget = `(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)`;
547
+ 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),${findTarget},?,?,?,?,CASE WHEN ${findTarget} IS NULL THEN 'unresolved' ELSE 'resolved' END,0.8,?,CASE WHEN ${findTarget} IS NULL THEN 'No local symbol target matched exactly' ELSE NULL END)`);
548
+ for (const r of rows) {
549
+ const targetArgs = [repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile];
550
+ stmt.run(
551
+ repoId,
552
+ repoId,
553
+ r.sourceFile,
554
+ r.callerQualifiedName,
555
+ ...targetArgs,
556
+ r.calleeExpression,
557
+ r.importSource,
558
+ r.sourceFile,
559
+ r.sourceLine,
560
+ ...targetArgs,
561
+ JSON.stringify(r.evidence),
562
+ ...targetArgs
563
+ );
564
+ }
549
565
  }
550
566
  function insertCalls(db, repoId, rows) {
551
567
  const stmt = db.prepare(
@@ -643,6 +659,10 @@ function isFunctionLike(node) {
643
659
  function exported(node) {
644
660
  return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
645
661
  }
662
+ function isPublicStaticMethod(node) {
663
+ const flags = ts.getCombinedModifierFlags(node);
664
+ return (flags & ts.ModifierFlags.Static) !== 0 && (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
665
+ }
646
666
  function exportDeclarations(source) {
647
667
  const exports = /* @__PURE__ */ new Map();
648
668
  const visit = (node) => {
@@ -663,14 +683,32 @@ function isObjectFunction(node) {
663
683
  var commonTerminalMembers = /* @__PURE__ */ new Set(["push", "includes", "find", "findIndex", "map", "filter", "reduce", "forEach", "some", "every", "toUpperCase", "toLowerCase", "trim", "split", "join", "get", "set", "has"]);
664
684
  var loggerMembers = /* @__PURE__ */ new Set(["trace", "debug", "info", "warn", "error", "fatal", "log"]);
665
685
  var globalObjects = /* @__PURE__ */ new Set(["JSON", "Object", "Array", "String", "Number", "Boolean", "Math", "Date", "Promise", "Reflect"]);
686
+ var capDslRoots = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPSERT", "UPDATE", "DELETE"]);
687
+ var requestHelpers = /* @__PURE__ */ new Set(["reject", "error", "info", "warn", "notify"]);
688
+ var transportMembers = /* @__PURE__ */ new Set(["emit", "publish", "send", "on"]);
666
689
  function callName(expr) {
667
690
  if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
668
691
  if (ts.isPropertyAccessExpression(expr)) {
669
692
  const left = expr.expression.getText();
670
- return { expression: expr.getText(), local: left === "this" ? void 0 : left, member: expr.name.text, receiver: left };
693
+ const root = left.split(".")[0];
694
+ return { expression: expr.getText(), local: left === "this" ? void 0 : root, member: expr.name.text, receiver: left };
671
695
  }
672
696
  return { expression: expr.getText() };
673
697
  }
698
+ function requireSource(expr) {
699
+ if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== "require") return void 0;
700
+ const first = expr.arguments[0];
701
+ return first && ts.isStringLiteral(first) ? first.text : void 0;
702
+ }
703
+ function ignoredFrameworkCall(callee) {
704
+ if (callee.local && capDslRoots.has(callee.local)) return true;
705
+ if (callee.expression === "cds.run" || callee.expression.startsWith("cds.connect.") || callee.expression.startsWith("cds.services.") || callee.expression.startsWith("cds.parse.")) return true;
706
+ if (callee.local === "req" && callee.member && requestHelpers.has(callee.member)) return true;
707
+ if (callee.member && transportMembers.has(callee.member)) return true;
708
+ if (callee.local && globalObjects.has(callee.local)) return true;
709
+ if (callee.expression.startsWith("new Date().")) return true;
710
+ return false;
711
+ }
674
712
  function nearest(symbols, line) {
675
713
  return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine))[0];
676
714
  }
@@ -683,11 +721,20 @@ async function parseExecutableSymbols(repoPath, filePath) {
683
721
  const imports = /* @__PURE__ */ new Map();
684
722
  const exportNames = exportDeclarations(source);
685
723
  const objectExports = /* @__PURE__ */ new Set();
686
- const addSymbol = (kind, localName, node, parentName, exportedName) => {
687
- const declaredExportName = exportedName ?? exportNames.get(parentName ? parentName.split(".")[0] ?? localName : localName);
724
+ const exportedClasses = /* @__PURE__ */ new Set();
725
+ const proxyVariables = /* @__PURE__ */ new Map();
726
+ const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
727
+ const parentRoot = parentName?.split(".")[0] ?? "";
728
+ const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
688
729
  const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
689
- const objectExported = parentName ? objectExports.has(parentName.split(".")[0] ?? "") : false;
690
- 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 });
730
+ const objectExported = parentName ? objectExports.has(parentRoot) : false;
731
+ const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && isPublicStaticMethod(node) : false;
732
+ const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
733
+ 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);
734
+ 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 });
735
+ };
736
+ const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
737
+ 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 } });
691
738
  };
692
739
  const visitImports = (node) => {
693
740
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
@@ -698,11 +745,21 @@ async function parseExecutableSymbols(repoPath, filePath) {
698
745
  if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
699
746
  if (named && ts.isNamespaceImport(named)) imports.set(named.name.text, sourceText);
700
747
  }
748
+ if (ts.isVariableStatement(node)) {
749
+ for (const declaration of node.declarationList.declarations) {
750
+ const requiredSource = declaration.initializer ? requireSource(declaration.initializer) : void 0;
751
+ if (ts.isIdentifier(declaration.name) && requiredSource) imports.set(declaration.name.text, requiredSource);
752
+ if (ts.isObjectBindingPattern(declaration.name) && requiredSource) {
753
+ for (const element of declaration.name.elements) if (ts.isIdentifier(element.name)) imports.set(element.name.text, requiredSource);
754
+ }
755
+ }
756
+ }
701
757
  ts.forEachChild(node, visitImports);
702
758
  };
703
759
  visitImports(source);
704
760
  const visitSymbols = (node, parentClass) => {
705
761
  if (ts.isClassDeclaration(node) && node.name) {
762
+ if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
706
763
  for (const member of node.members) visitSymbols(member, node.name.text);
707
764
  return;
708
765
  }
@@ -716,8 +773,10 @@ async function parseExecutableSymbols(repoPath, filePath) {
716
773
  if (!localName || !d.initializer) continue;
717
774
  if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
718
775
  if (ts.isObjectLiteralExpression(d.initializer)) {
719
- if (exported(node) || exportNames.has(localName)) objectExports.add(localName);
776
+ const objectIsExported = exported(node) || exportNames.has(localName);
777
+ if (objectIsExported) objectExports.add(localName);
720
778
  for (const prop of d.initializer.properties) {
779
+ if (objectIsExported && ts.isShorthandPropertyAssignment(prop)) addAliasSymbol(localName, prop.name.text, prop.name, imports.get(prop.name.text));
721
780
  if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
722
781
  const propName = nameOf(prop.name);
723
782
  if (propName) addSymbol("object_method", propName, prop.initializer, localName);
@@ -731,6 +790,15 @@ async function parseExecutableSymbols(repoPath, filePath) {
731
790
  } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
732
791
  };
733
792
  visitSymbols(source);
793
+ const visitProxyVariables = (node) => {
794
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
795
+ const callee = callName(node.initializer.expression);
796
+ const importSource = callee.local ? imports.get(callee.local) : void 0;
797
+ if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression });
798
+ }
799
+ ts.forEachChild(node, visitProxyVariables);
800
+ };
801
+ visitProxyVariables(source);
734
802
  const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
735
803
  const visitCalls = (node) => {
736
804
  if (ts.isCallExpression(node)) {
@@ -738,19 +806,21 @@ async function parseExecutableSymbols(repoPath, filePath) {
738
806
  const caller = nearest(symbols, line);
739
807
  if (caller) {
740
808
  const callee = callName(node.expression);
741
- const importSource = (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
742
- const targetName = callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
809
+ const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
810
+ const importSource = proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
811
+ const targetName = proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
743
812
  const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
744
813
  const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
745
814
  const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
746
815
  const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
747
816
  const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
748
817
  const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
749
- const provenRelativeImport = Boolean(isRelativeImport(importSource));
750
- const globalLike = callee.local ? globalObjects.has(callee.local) : false;
818
+ const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
751
819
  const importedFromPackage = Boolean(importSource && !isRelativeImport(importSource));
752
- const keep = Boolean(targetName) && !loggerLike && !globalLike && !importedFromPackage && (provenLocal || provenThisMethod || provenRelativeImport || !terminalMember && !callee.expression.startsWith("this."));
753
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: provenThisMethod ? thisTarget : targetName, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : provenLocal ? "indexed_local_symbol" : "unresolved_actionable_property_call", caller: caller.qualifiedName, targetName: provenThisMethod ? thisTarget : targetName } });
820
+ const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
821
+ const resolvedTarget = provenThisMethod ? thisTarget : targetName;
822
+ const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport);
823
+ 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, factory: proxy?.factory } });
754
824
  }
755
825
  }
756
826
  ts.forEachChild(node, visitCalls);
@@ -905,14 +975,10 @@ function location(evidence) {
905
975
  }
906
976
  return ":";
907
977
  }
908
- function displayTarget(value, evidence) {
909
- if (/^\d+$/.test(value) && evidence.parserWarning) return "Entity: unknown";
910
- return value;
911
- }
912
978
  function renderTraceTable(result) {
913
979
  const lines = ["Step Type From To Evidence"];
914
980
  for (const e of result.edges) {
915
- lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${displayTarget(e.to, e.evidence).slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
981
+ lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
916
982
  }
917
983
  if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.map((d) => `${String(d.severity ?? "info")} ${String(d.code ?? "diagnostic")} ${String(d.message ?? "")}`));
918
984
  return `${lines.join("\n")}
@@ -933,8 +999,6 @@ function safe(value) {
933
999
  return value.replace(/[^\w-]/g, "_").slice(0, 60);
934
1000
  }
935
1001
  function label(trace2, idOrLabel) {
936
- const edge = trace2.edges.find((item) => item.to === idOrLabel);
937
- if (/^\d+$/.test(idOrLabel) && edge?.evidence.parserWarning) return "Entity: unknown";
938
1002
  const node = trace2.nodes.find((item) => item.id === idOrLabel || item.label === idOrLabel);
939
1003
  return String(node?.label ?? idOrLabel);
940
1004
  }
@@ -1007,7 +1071,15 @@ function localServiceDiagnostics(db, strict) {
1007
1071
  const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
1008
1072
  const withoutOwnership = rows.filter((row) => row.reason === "local_service_candidate_without_caller_ownership" || String(row.evidenceJson ?? "").includes("local_service_candidate_without_caller_ownership")).length;
1009
1073
  const unresolved = rows.filter((row) => row.status === "unresolved").length;
1010
- const outsideScope = rows.filter((row) => row.status === "unresolved" && String(row.evidenceJson ?? "").includes("candidateCount")).length;
1074
+ const outsideScope = rows.filter((row) => {
1075
+ if (row.status !== "unresolved") return false;
1076
+ try {
1077
+ const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1078
+ return Number(evidence.candidateCount ?? 0) > 0;
1079
+ } catch {
1080
+ return false;
1081
+ }
1082
+ }).length;
1011
1083
  const out = [];
1012
1084
  if (withoutOwnership > 0) out.push({ severity: "warning", code: "local_service_candidate_without_caller_ownership", message: `Local service calls have operation candidates but no caller ownership evidence: ${withoutOwnership}` });
1013
1085
  if (outsideScope > 0) out.push({ severity: "warning", code: "local_service_candidates_outside_local_scope", message: `Local service calls found candidates outside same-repository scope: ${outsideScope}` });
@@ -1015,6 +1087,32 @@ function localServiceDiagnostics(db, strict) {
1015
1087
  if (strict && implementationContext > 0) out.push({ severity: "info", code: "local_service_calls_resolved_by_implementation_context", message: `Local service calls resolved by implementation-context ownership: ${implementationContext}` });
1016
1088
  return out;
1017
1089
  }
1090
+ function parserQualityDiagnostics(db, strict) {
1091
+ if (!strict) return [];
1092
+ const symbolUnresolvedThreshold = 0.05;
1093
+ const dbUnknownThreshold = 0.25;
1094
+ const outboundUnownedThreshold = 0.01;
1095
+ 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();
1096
+ 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();
1097
+ 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();
1098
+ 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();
1099
+ 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();
1100
+ const symbolTotal = Number(symbol.total ?? 0);
1101
+ const symbolUnresolved = Number(symbol.unresolved ?? 0);
1102
+ const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
1103
+ const queryTotal = Number(dbq.total ?? 0);
1104
+ const queryUnknown = Number(dbq.unknown ?? 0);
1105
+ const queryUnknownRatio = queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4));
1106
+ const outboundTotal = Number(outbound.total ?? 0);
1107
+ const outboundWithoutOwnership = Number(outbound.withoutOwnership ?? 0);
1108
+ const outboundWithoutOwnershipRatio = outboundTotal === 0 ? 0 : Number((outboundWithoutOwnership / outboundTotal).toFixed(4));
1109
+ return [
1110
+ { 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) },
1111
+ { 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 },
1112
+ { 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 },
1113
+ { 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 }
1114
+ ];
1115
+ }
1018
1116
  function createProgram() {
1019
1117
  const program = new Command();
1020
1118
  program.name("service-flow").description(
@@ -1225,7 +1323,8 @@ function createProgram() {
1225
1323
  FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
1226
1324
  ).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
1227
1325
  const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
1228
- const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth];
1326
+ const parserQualityHealth = parserQualityDiagnostics(db, Boolean(opts.strict));
1327
+ const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...parserQualityHealth];
1229
1328
  process.stdout.write(
1230
1329
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
1231
1330
  `