@saptools/service-flow 0.1.15 → 0.1.17

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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ containsSupportedOutboundCall,
3
4
  discoverRepositories,
4
5
  linkWorkspace,
5
6
  normalizePath,
@@ -10,7 +11,7 @@ import {
10
11
  parsePackageJson,
11
12
  parseServiceBindings,
12
13
  trace
13
- } from "./chunk-ZSUL2X77.js";
14
+ } from "./chunk-HBR27SPD.js";
14
15
 
15
16
  // src/cli.ts
16
17
  import { Command } from "commander";
@@ -100,7 +101,7 @@ CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTE
100
101
  CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
101
102
  CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
102
103
  CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
103
- CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
104
+ CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
104
105
  CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
105
106
  CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
106
107
  CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
@@ -114,7 +115,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, rep
114
115
  `;
115
116
 
116
117
  // src/db/migrations.ts
117
- var CURRENT_SCHEMA_VERSION = 4;
118
+ var CURRENT_SCHEMA_VERSION = 5;
118
119
  var columns = {
119
120
  service_bindings: [
120
121
  { name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
@@ -145,7 +146,8 @@ var columns = {
145
146
  outbound_calls: [
146
147
  { name: "local_service_name", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_name TEXT" },
147
148
  { name: "local_service_lookup", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_lookup TEXT" },
148
- { name: "alias_chain_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT" }
149
+ { name: "alias_chain_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT" },
150
+ { name: "evidence_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN evidence_json TEXT" }
149
151
  ],
150
152
  index_runs: [
151
153
  { name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" }
@@ -185,7 +187,7 @@ function migrate(db) {
185
187
  // package.json
186
188
  var package_default = {
187
189
  name: "@saptools/service-flow",
188
- version: "0.1.15",
190
+ version: "0.1.17",
189
191
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
190
192
  type: "module",
191
193
  publishConfig: {
@@ -543,35 +545,44 @@ function insertExecutableSymbols(db, repoId, rows) {
543
545
  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
546
  }
545
547
  function insertSymbolCalls(db, repoId, rows) {
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
+ const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
549
+ 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
550
  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
- );
551
+ const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
552
+ const target = resolveSymbolCallTarget(db, repoId, r);
553
+ 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);
554
+ }
555
+ }
556
+ function resolveSymbolCallTarget(db, repoId, r) {
557
+ const evidence = r.evidence;
558
+ 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);
559
+ if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "same_file_exact", candidateCount: 1 };
560
+ if (localRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: localRows.length };
561
+ 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);
562
+ if (evidence.relation === "relative_import_proxy_member" && rows.length > 1) {
563
+ const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
564
+ if (objectMapRows.length > 0) {
565
+ const concrete = rows.find((row) => row.kind !== "object_alias") ?? objectMapRows[0];
566
+ return { id: concrete?.id ?? null, status: "resolved", reason: null, strategy: "proxy_member_exported_object_map", candidateCount: rows.length };
567
+ }
568
+ 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 };
564
569
  }
570
+ 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 };
571
+ if (rows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple exported symbol targets matched exactly", strategy: "exported_exact", candidateCount: rows.length };
572
+ 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 };
565
573
  }
566
574
  function insertCalls(db, repoId, rows) {
567
575
  const stmt = db.prepare(
568
- "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))"
576
+ "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,evidence_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))"
569
577
  );
570
578
  for (const r of rows)
571
579
  stmt.run(
572
580
  repoId,
573
581
  repoId,
574
582
  r.sourceFile,
583
+ r.sourceSymbolQualifiedName,
584
+ repoId,
585
+ r.sourceFile,
575
586
  r.sourceLine,
576
587
  r.sourceLine,
577
588
  r.callType,
@@ -587,6 +598,7 @@ function insertCalls(db, repoId, rows) {
587
598
  r.localServiceName,
588
599
  r.localServiceLookup,
589
600
  r.aliasChain ? JSON.stringify(r.aliasChain) : null,
601
+ r.evidence ? JSON.stringify(r.evidence) : null,
590
602
  repoId,
591
603
  r.serviceVariableName,
592
604
  r.sourceFile,
@@ -766,6 +778,9 @@ async function parseExecutableSymbols(repoPath, filePath) {
766
778
  if (ts.isMethodDeclaration(node)) {
767
779
  const localName = nameOf(node.name);
768
780
  if (localName) addSymbol("method", localName, node, parentClass);
781
+ } else if (ts.isPropertyDeclaration(node) && parentClass && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
782
+ const localName = nameOf(node.name);
783
+ 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" });
769
784
  } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
770
785
  else if (ts.isVariableStatement(node)) {
771
786
  for (const d of node.declarationList.declarations) {
@@ -790,11 +805,41 @@ async function parseExecutableSymbols(repoPath, filePath) {
790
805
  } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
791
806
  };
792
807
  visitSymbols(source);
808
+ const isTopLevelCallback = (node) => {
809
+ if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
810
+ if (!ts.isCallExpression(node.parent)) return false;
811
+ const callee = callName(node.parent.expression);
812
+ const member = callee.member ?? callee.local;
813
+ return Boolean(member && ["bootstrap", "served", "connect", "on", "once", "use", "get", "post", "put", "patch", "delete", "subscribe"].includes(member));
814
+ };
815
+ const visitCallbackSymbols = (node) => {
816
+ if (isTopLevelCallback(node) && containsSupportedOutboundCall(node)) {
817
+ const startLine = lineOf(source, node.getStart(source));
818
+ const name = `callback:${startLine}`;
819
+ 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 } });
820
+ }
821
+ ts.forEachChild(node, visitCallbackSymbols);
822
+ };
823
+ visitCallbackSymbols(source);
824
+ const visitEventRegistrationSymbols = (node) => {
825
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "on") {
826
+ const receiver = node.expression.expression.getText(source);
827
+ const eventArg = node.arguments[0];
828
+ if ((receiver === "cds" || /^(srv|service|serviceClient|messaging|messageClient|eventClient|.*Client)$/.test(receiver)) && eventArg && (ts.isStringLiteral(eventArg) || ts.isNoSubstitutionTemplateLiteral(eventArg))) {
829
+ const startLine = lineOf(source, node.getStart(source));
830
+ const eventName = eventArg.text.replace(/[^A-Za-z0-9_$-]/g, "_");
831
+ const name = `event:${eventName}:${startLine}`;
832
+ symbols.push({ kind: "event_registration", 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_event_registration", eventName: eventArg.text, registrationLine: startLine, receiver } });
833
+ }
834
+ }
835
+ ts.forEachChild(node, visitEventRegistrationSymbols);
836
+ };
837
+ visitEventRegistrationSymbols(source);
793
838
  const visitProxyVariables = (node) => {
794
839
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
795
840
  const callee = callName(node.initializer.expression);
796
841
  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 });
842
+ if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression, variableName: node.name.text });
798
843
  }
799
844
  ts.forEachChild(node, visitProxyVariables);
800
845
  };
@@ -820,7 +865,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
820
865
  const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
821
866
  const resolvedTarget = provenThisMethod ? thisTarget : targetName;
822
867
  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 } });
868
+ 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 } });
824
869
  }
825
870
  }
826
871
  ts.forEachChild(node, visitCalls);
@@ -1097,6 +1142,25 @@ function parserQualityDiagnostics(db, strict) {
1097
1142
  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
1143
  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
1144
  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();
1145
+ 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();
1146
+ const ownerlessByCategory = db.prepare(`SELECT CASE
1147
+ WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
1148
+ WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
1149
+ WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
1150
+ WHEN EXISTS (SELECT 1 FROM symbols s WHERE s.repo_id=outbound_calls.repo_id AND s.source_file=outbound_calls.source_file) THEN 'line_range_mismatch'
1151
+ WHEN source_line <= 1 THEN 'unsupported_function_shape'
1152
+ WHEN source_line > 1 THEN 'unsupported_callback_shape'
1153
+ ELSE 'unknown' END category, COUNT(*) count
1154
+ FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY category ORDER BY count DESC, category`).all();
1155
+ const ownerlessExamples = db.prepare(`SELECT CASE
1156
+ WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
1157
+ WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
1158
+ WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
1159
+ WHEN EXISTS (SELECT 1 FROM symbols s WHERE s.repo_id=outbound_calls.repo_id AND s.source_file=outbound_calls.source_file) THEN 'line_range_mismatch'
1160
+ WHEN source_line <= 1 THEN 'unsupported_function_shape'
1161
+ WHEN source_line > 1 THEN 'unsupported_callback_shape'
1162
+ ELSE 'unknown' END category, call_type callType, source_file sourceFile, source_line sourceLine, unresolved_reason unresolvedReason
1163
+ FROM outbound_calls WHERE source_symbol_id IS NULL ORDER BY category, source_file, source_line LIMIT 10`).all();
1100
1164
  const symbolTotal = Number(symbol.total ?? 0);
1101
1165
  const symbolUnresolved = Number(symbol.unresolved ?? 0);
1102
1166
  const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
@@ -1110,7 +1174,7 @@ function parserQualityDiagnostics(db, strict) {
1110
1174
  { 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
1175
  { 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
1176
  { 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 }
1177
+ { 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, ownerlessByCategory, ownerlessExamples }
1114
1178
  ];
1115
1179
  }
1116
1180
  function createProgram() {
@@ -1137,7 +1201,7 @@ function createProgram() {
1137
1201
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
1138
1202
  const r = linkWorkspace(db, workspaceId);
1139
1203
  process.stdout.write(
1140
- `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
1204
+ `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
1141
1205
  `
1142
1206
  );
1143
1207
  }).catch(fail)