@saptools/service-flow 0.1.42 → 0.1.44

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
@@ -13,7 +13,7 @@ import {
13
13
  parsePackageJson,
14
14
  parseServiceBindings,
15
15
  trace
16
- } from "./chunk-RP3BJ64F.js";
16
+ } from "./chunk-UKNPHTUS.js";
17
17
 
18
18
  // src/cli.ts
19
19
  import { Command } from "commander";
@@ -209,7 +209,7 @@ function migrate(db) {
209
209
  // package.json
210
210
  var package_default = {
211
211
  name: "@saptools/service-flow",
212
- version: "0.1.42",
212
+ version: "0.1.44",
213
213
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
214
214
  type: "module",
215
215
  publishConfig: {
@@ -1057,37 +1057,46 @@ function sha256Text(text) {
1057
1057
  }
1058
1058
 
1059
1059
  // src/indexer/repository-indexer.ts
1060
- async function indexRepository(db, repo, force) {
1061
- try {
1062
- const sourceFiles = await findSourceFiles(repo.absolute_path);
1063
- const packageFacts = await parsePackageJson(repo.absolute_path);
1064
- const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, packageFacts);
1065
- if (!force && repo.fingerprint === fingerprint) return { fileCount: 0, diagnosticCount: 0, skipped: true };
1066
- const kind = await classifyRepository(repo.absolute_path, packageFacts);
1067
- const parsed = await parseAllSourceFacts(repo.absolute_path, sourceFiles);
1068
- db.transaction(() => {
1069
- db.prepare("UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?").run(packageFacts.packageName, packageFacts.packageVersion, JSON.stringify(packageFacts.dependencies), kind, "indexing", repo.id);
1070
- clearRepoFacts(db, repo.id);
1071
- insertRequires(db, repo.id, packageFacts.cdsRequires);
1072
- const fileStmt = db.prepare("INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at");
1073
- for (const file of parsed.fileRecords) fileStmt.run(repo.id, file.relativePath, file.extension, file.sha256, file.sizeBytes, (/* @__PURE__ */ new Date()).toISOString());
1074
- for (const s of parsed.services) insertService(db, repo.id, s);
1075
- for (const h of parsed.handlers) insertHandler(db, repo.id, h);
1076
- insertExecutableSymbols(db, repo.id, parsed.symbols);
1077
- insertSymbolCalls(db, repo.id, parsed.symbolCalls);
1078
- insertRegistrations(db, repo.id, parsed.registrations);
1079
- insertBindings(db, repo.id, parsed.bindings);
1080
- insertCalls(db, repo.id, parsed.calls);
1081
- db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fingerprint, (/* @__PURE__ */ new Date()).toISOString(), ANALYZER_VERSION, repo.id);
1082
- });
1083
- return { fileCount: sourceFiles.length, diagnosticCount: 0, skipped: false };
1084
- } catch (error) {
1085
- const message = errorMessage(error);
1086
- db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repo.id);
1087
- db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repo.id);
1088
- db.prepare("INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)").run(repo.id, "error", "source_read_failed", `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
1089
- return { fileCount: 0, diagnosticCount: 1, skipped: false };
1090
- }
1060
+ async function prepareRepositoryIndex(repo, force) {
1061
+ const sourceFiles = await findSourceFiles(repo.absolute_path);
1062
+ const packageFacts = await parsePackageJson(repo.absolute_path);
1063
+ const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, packageFacts);
1064
+ if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
1065
+ return {
1066
+ repo,
1067
+ packageFacts,
1068
+ fingerprint,
1069
+ kind: await classifyRepository(repo.absolute_path, packageFacts),
1070
+ parsed: await parseAllSourceFacts(repo.absolute_path, sourceFiles),
1071
+ fileCount: sourceFiles.length,
1072
+ diagnosticCount: 0,
1073
+ skipped: false
1074
+ };
1075
+ }
1076
+ function publishPreparedRepositoryIndex(db, prepared) {
1077
+ if (prepared.skipped) return;
1078
+ if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint || !prepared.kind) throw new Error("Prepared repository index is missing publication facts");
1079
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1080
+ const repoId = prepared.repo.id;
1081
+ db.prepare("UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?").run(prepared.packageFacts.packageName, prepared.packageFacts.packageVersion, JSON.stringify(prepared.packageFacts.dependencies), prepared.kind, "indexing", repoId);
1082
+ clearRepoFacts(db, repoId);
1083
+ insertRequires(db, repoId, prepared.packageFacts.cdsRequires);
1084
+ const fileStmt = db.prepare("INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at");
1085
+ for (const file of prepared.parsed.fileRecords) fileStmt.run(repoId, file.relativePath, file.extension, file.sha256, file.sizeBytes, now);
1086
+ for (const service of prepared.parsed.services) insertService(db, repoId, service);
1087
+ for (const handler of prepared.parsed.handlers) insertHandler(db, repoId, handler);
1088
+ insertExecutableSymbols(db, repoId, prepared.parsed.symbols);
1089
+ insertSymbolCalls(db, repoId, prepared.parsed.symbolCalls);
1090
+ insertRegistrations(db, repoId, prepared.parsed.registrations);
1091
+ insertBindings(db, repoId, prepared.parsed.bindings);
1092
+ insertCalls(db, repoId, prepared.parsed.calls);
1093
+ db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run(now, prepared.fingerprint, now, ANALYZER_VERSION, repoId);
1094
+ }
1095
+ function recordIndexFailure(db, repoId, error) {
1096
+ const message = errorMessage(error);
1097
+ db.prepare("UPDATE repositories SET index_status='failed', error_count=1 WHERE id=?").run(repoId);
1098
+ db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
1099
+ db.prepare("INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)").run(repoId, "error", "source_read_failed", `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
1091
1100
  }
1092
1101
  async function parseAllSourceFacts(root, files) {
1093
1102
  const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
@@ -1151,36 +1160,66 @@ async function repositoryFingerprint(root, files, facts) {
1151
1160
  function materializeCdsExtensionOperations(db, workspaceId) {
1152
1161
  const extensions = db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,s.qualified_name qualifiedName,s.source_file sourceFile,s.extension_module_specifier moduleSpecifier,s.extension_imported_symbol importedSymbol,s.extension_import_kind importKind
1153
1162
  FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=1`).all(workspaceId);
1154
- for (const extension of extensions) {
1155
- const bases = resolveBase(db, workspaceId, extension);
1156
- const status = bases.length === 1 ? "resolved" : bases.length > 1 ? "ambiguous" : "unresolved";
1157
- db.prepare("UPDATE cds_services SET extension_base_status=?, extension_base_service_id=? WHERE id=?").run(status, status === "resolved" ? bases[0]?.id : null, extension.id);
1158
- if (bases.length !== 1) {
1159
- db.prepare("DELETE FROM cds_operations WHERE service_id=? AND provenance='inherited'").run(extension.id);
1160
- db.prepare("DELETE FROM search_index WHERE repo=? AND kind='operation' AND name NOT IN (SELECT operation_name FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE s.repo_id=?)").run(String(extension.repoId), extension.repoId);
1161
- continue;
1163
+ db.transaction(() => {
1164
+ const changedRepos = /* @__PURE__ */ new Set();
1165
+ for (const extension of extensions) {
1166
+ if (reconcileExtension(db, workspaceId, extension)) changedRepos.add(extension.repoId);
1162
1167
  }
1163
- reconcileInheritedOperations(db, extension, bases[0]);
1164
- }
1168
+ for (const repoId of changedRepos) markRepositoryDerivedFactsChanged(db, repoId);
1169
+ });
1165
1170
  }
1166
- function reconcileInheritedOperations(db, extension, base) {
1167
- const existing = db.prepare("SELECT id,operation_name operationName,operation_path operationPath,base_operation_id baseOperationId FROM cds_operations WHERE service_id=? AND provenance='inherited'").all(extension.id);
1168
- const desired = db.prepare("SELECT id,operation_type operationType,operation_name operationName,operation_path operationPath,params_json paramsJson,return_type returnType,source_file sourceFile,source_line sourceLine FROM cds_operations WHERE service_id=? AND provenance='direct' AND NOT EXISTS (SELECT 1 FROM cds_operations direct WHERE direct.service_id=? AND direct.provenance='direct' AND direct.operation_name=cds_operations.operation_name AND direct.operation_path=cds_operations.operation_path)").all(base.id, extension.id);
1169
- const desiredKeys = new Set(desired.map((row) => `${String(row.operationName)}\0${String(row.operationPath)}`));
1171
+ function reconcileExtension(db, workspaceId, extension) {
1172
+ const bases = resolveBase(db, workspaceId, extension);
1173
+ const status = bases.length === 1 ? "resolved" : bases.length > 1 ? "ambiguous" : "unresolved";
1174
+ const baseId = status === "resolved" ? bases[0]?.id ?? null : null;
1175
+ const desired = baseId === null ? [] : desiredInheritedOperations(db, extension, baseId);
1176
+ const statusChanged = updateExtensionStatus(db, extension.id, status, baseId);
1177
+ const operationChanged = reconcileInheritedOperations(db, extension, desired);
1178
+ reconcileOperationSearchRows(db, extension.repoId);
1179
+ return statusChanged || operationChanged;
1180
+ }
1181
+ function desiredInheritedOperations(db, extension, baseId) {
1182
+ return db.prepare("SELECT id,operation_type operationType,operation_name operationName,operation_path operationPath,params_json paramsJson,return_type returnType,source_file sourceFile,source_line sourceLine FROM cds_operations WHERE service_id=? AND provenance='direct' AND NOT EXISTS (SELECT 1 FROM cds_operations direct WHERE direct.service_id=? AND direct.provenance='direct' AND direct.operation_name=cds_operations.operation_name AND direct.operation_path=cds_operations.operation_path) ORDER BY operation_name,operation_path,id").all(baseId, extension.id);
1183
+ }
1184
+ function updateExtensionStatus(db, serviceId, status, baseId) {
1185
+ const row = db.prepare("SELECT extension_base_status status,extension_base_service_id baseId FROM cds_services WHERE id=?").get(serviceId);
1186
+ if (row?.status === status && (row.baseId ?? null) === baseId) return false;
1187
+ db.prepare("UPDATE cds_services SET extension_base_status=?, extension_base_service_id=? WHERE id=?").run(status, baseId, serviceId);
1188
+ return true;
1189
+ }
1190
+ function reconcileInheritedOperations(db, extension, desired) {
1191
+ const existing = db.prepare("SELECT id,operation_type operationType,operation_name operationName,operation_path operationPath,params_json paramsJson,return_type returnType,source_file sourceFile,source_line sourceLine,base_operation_id baseOperationId FROM cds_operations WHERE service_id=? AND provenance='inherited'").all(extension.id);
1192
+ const desiredKeys = new Set(desired.map((row) => `${row.operationName}\0${row.operationPath}`));
1170
1193
  const byKey = new Map(existing.map((row) => [`${row.operationName}\0${row.operationPath}`, row]));
1194
+ let changed = false;
1171
1195
  for (const row of existing) {
1172
- if (!desiredKeys.has(`${row.operationName}\0${row.operationPath}`)) db.prepare("DELETE FROM cds_operations WHERE id=?").run(row.id);
1196
+ if (desiredKeys.has(`${row.operationName}\0${row.operationPath}`)) continue;
1197
+ db.prepare("DELETE FROM cds_operations WHERE id=?").run(row.id);
1198
+ changed = true;
1173
1199
  }
1174
1200
  const update = db.prepare("UPDATE cds_operations SET operation_type=?,params_json=?,return_type=?,source_file=?,source_line=?,base_operation_id=? WHERE id=?");
1175
1201
  const add = db.prepare("INSERT INTO cds_operations(service_id,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line,provenance,base_operation_id) VALUES(?,?,?,?,?,?,?,?,'inherited',?)");
1176
- const search = db.prepare("INSERT INTO search_index(kind,name,path,repo) SELECT ?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM search_index WHERE kind=? AND name=? AND path=? AND repo=?)");
1177
1202
  for (const row of desired) {
1178
- const key = `${String(row.operationName)}\0${String(row.operationPath)}`;
1179
- const current = byKey.get(key);
1203
+ const current = byKey.get(`${row.operationName}\0${row.operationPath}`);
1204
+ if (current && operationMatches(current, row)) continue;
1180
1205
  if (current) update.run(row.operationType, row.paramsJson, row.returnType, row.sourceFile, row.sourceLine, row.id, current.id);
1181
1206
  else add.run(extension.id, row.operationType, row.operationName, row.operationPath, row.paramsJson, row.returnType, row.sourceFile, row.sourceLine, row.id);
1182
- search.run("operation", row.operationName, row.operationPath, String(extension.repoId), "operation", row.operationName, row.operationPath, String(extension.repoId));
1207
+ changed = true;
1183
1208
  }
1209
+ return changed;
1210
+ }
1211
+ function operationMatches(current, desired) {
1212
+ return current.operationType === desired.operationType && current.paramsJson === desired.paramsJson && current.returnType === desired.returnType && current.sourceFile === desired.sourceFile && current.sourceLine === desired.sourceLine && current.baseOperationId === desired.id;
1213
+ }
1214
+ function reconcileOperationSearchRows(db, repoId) {
1215
+ db.prepare("DELETE FROM search_index WHERE repo=? AND kind='operation'").run(String(repoId));
1216
+ db.prepare(`INSERT INTO search_index(kind,name,path,repo)
1217
+ SELECT 'operation',o.operation_name,o.operation_path,?
1218
+ FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
1219
+ WHERE s.repo_id=? ORDER BY o.operation_name,o.operation_path,o.id`).run(String(repoId), repoId);
1220
+ }
1221
+ function markRepositoryDerivedFactsChanged(db, repoId) {
1222
+ db.prepare("UPDATE repositories SET fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='derived_facts_changed', graph_stale_at=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), repoId);
1184
1223
  }
1185
1224
  function resolveBase(db, workspaceId, extension) {
1186
1225
  const symbol = extension.importedSymbol ?? extension.serviceName;
@@ -1224,22 +1263,387 @@ async function indexWorkspace(db, workspaceId, options) {
1224
1263
  let fileCount = 0;
1225
1264
  let diagnosticCount = 0;
1226
1265
  let skippedCount = 0;
1266
+ const preparedRows = [];
1267
+ let activeRepoId;
1227
1268
  try {
1228
1269
  for (const repo of repos) {
1229
- const result = await indexRepository(db, repo, options.force);
1270
+ activeRepoId = repo.id;
1271
+ const result = await prepareRepositoryIndex(repo, options.force);
1272
+ preparedRows.push(result);
1230
1273
  fileCount += result.fileCount;
1231
1274
  diagnosticCount += result.diagnosticCount;
1232
1275
  skippedCount += result.skipped ? 1 : 0;
1233
1276
  }
1234
- materializeCdsExtensionOperations(db, workspaceId);
1235
- db.prepare("UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), diagnosticCount ? "failed" : "success", fileCount, diagnosticCount, runId);
1277
+ db.transaction(() => {
1278
+ for (const row of preparedRows) {
1279
+ activeRepoId = row.repo.id;
1280
+ publishPreparedRepositoryIndex(db, row);
1281
+ }
1282
+ if (options.injectDerivedMaterializationFailure) throw new Error("Injected derived materialization failure");
1283
+ materializeCdsExtensionOperations(db, workspaceId);
1284
+ db.prepare("UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), diagnosticCount ? "failed" : "success", fileCount, diagnosticCount, runId);
1285
+ });
1236
1286
  return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
1237
1287
  } catch (error) {
1288
+ if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
1238
1289
  db.prepare("UPDATE index_runs SET finished_at=?, status='failed', file_count=?, diagnostic_count=?, error_message=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fileCount, diagnosticCount + 1, errorMessage(error), runId);
1239
1290
  throw error;
1240
1291
  }
1241
1292
  }
1242
1293
 
1294
+ // src/cli/doctor.ts
1295
+ function linkUpgradeWarnings(db) {
1296
+ return [...schemaDriftDiagnostics(db, true), ...analyzerVersionDiagnostics(db, true)].filter((item) => ["schema_legacy_columns_present", "external_target_columns_missing_data", "reindex_required_after_upgrade", "reindex_required_after_analyzer_upgrade"].includes(String(item.code)));
1297
+ }
1298
+ function doctorDiagnostics(db, strict) {
1299
+ const diagnostics = db.prepare("SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id").all();
1300
+ return [
1301
+ ...diagnostics,
1302
+ ...healthDiagnostics(db, strict),
1303
+ ...localServiceDiagnostics(db, strict),
1304
+ ...schemaDriftDiagnostics(db, strict),
1305
+ ...analyzerVersionDiagnostics(db, strict),
1306
+ ...parserQualityDiagnostics(db, strict)
1307
+ ];
1308
+ }
1309
+ function healthDiagnostics(db, strict) {
1310
+ return db.prepare(
1311
+ `SELECT 'info' severity,'entity_only_service' code,'CDS service has no action/function/event operations; this can be valid for entity-only services' message,s.source_file sourceFile,s.source_line sourceLine
1312
+ FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
1313
+ UNION ALL
1314
+ SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
1315
+ FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND s.is_extend=1 AND ?
1316
+ UNION ALL
1317
+ SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
1318
+ FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
1319
+ WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
1320
+ UNION ALL
1321
+ SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
1322
+ WHERE NOT EXISTS (SELECT 1 FROM search_index)
1323
+ UNION ALL
1324
+ SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
1325
+ WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)
1326
+ UNION ALL
1327
+ SELECT 'warning','legacy_schema_weaker_foreign_keys','Legacy table lacks fresh-schema foreign-key metadata; rebuild the database or re-run init/index in a new database',NULL,NULL
1328
+ WHERE (SELECT COUNT(*) FROM pragma_foreign_key_list('graph_edges'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('index_runs'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('diagnostics'))=0
1329
+ UNION ALL
1330
+ SELECT 'warning','remote_target_without_implementation','Remote target operation has no implementation edge: ' || s.service_path || o.operation_path,o.source_file,o.source_line
1331
+ FROM graph_edges remote JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER) JOIN cds_services s ON s.id=o.service_id
1332
+ WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' AND NOT EXISTS (SELECT 1 FROM graph_edges impl WHERE impl.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND impl.from_kind='operation' AND impl.from_id=remote.to_id) AND ?
1333
+ UNION ALL
1334
+ SELECT CASE WHEN ? THEN 'warning' ELSE 'error' END,'local_service_calls_all_unresolved','All local service calls are unresolved; verify local service alias parsing and linking',NULL,NULL
1335
+ WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE call_type='local_service_call') AND NOT EXISTS (SELECT 1 FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call' AND e.status='resolved')
1336
+ UNION ALL
1337
+ SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
1338
+ FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
1339
+ UNION ALL
1340
+ SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
1341
+ FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
1342
+ UNION ALL
1343
+ SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
1344
+ WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
1345
+ UNION ALL
1346
+ SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
1347
+ WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
1348
+ UNION ALL
1349
+ SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
1350
+ FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
1351
+ ).all(strict, strict, strict, strict, strict, strict, strict);
1352
+ }
1353
+ function schemaDriftDiagnostics(db, strict) {
1354
+ if (!strict) return [];
1355
+ const columns2 = db.prepare("PRAGMA table_info(symbols)").all();
1356
+ const legacy = columns2.filter((row) => ["external_target_kind", "external_target_id", "external_target_label", "external_target_dynamic"].includes(String(row.name))).map((row) => row.name);
1357
+ const missing = db.prepare("SELECT id id,source_file sourceFile,source_line sourceLine FROM outbound_calls WHERE call_type='external_http' AND (external_target_id IS NULL OR external_target_label IS NULL OR external_target_kind IS NULL) LIMIT 20").all();
1358
+ const out = [];
1359
+ if (legacy.length > 0) out.push({ severity: "warning", code: "schema_legacy_columns_present", message: "Legacy external-target columns are present on symbols; run service-flow clean --db-only, then init/index/link to rebuild with the current schema.", scope: "workspace", affectedColumns: legacy, remediation: "service-flow clean --db-only && service-flow init <workspace> && service-flow index && service-flow link" });
1360
+ if (missing.length > 0) out.push({ severity: "warning", code: "external_target_columns_missing_data", message: "External HTTP calls are missing queryable external target metadata; reindex is required after upgrade.", scope: "workspace", affectedRows: missing, remediation: "service-flow index --force && service-flow link" });
1361
+ if (legacy.length > 0 || missing.length > 0) out.push({ severity: "warning", code: "reindex_required_after_upgrade", message: "This database cannot be made equivalent to a fresh index by relink alone.", scope: "workspace", remediation: "Rebuild or force reindex the workspace, then run service-flow doctor --strict." });
1362
+ return out;
1363
+ }
1364
+ function analyzerVersionDiagnostics(db, strict) {
1365
+ if (!strict) return [];
1366
+ const rows = db.prepare("SELECT name,COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion FROM repositories WHERE index_status='indexed' AND COALESCE(fact_analyzer_version,'legacy')<>?").all(ANALYZER_VERSION);
1367
+ if (rows.length === 0) return [];
1368
+ return [{ severity: "warning", code: "reindex_required_after_analyzer_upgrade", message: "Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.", scope: "workspace", affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: "service-flow index --force && service-flow link" }];
1369
+ }
1370
+ function localServiceDiagnostics(db, strict) {
1371
+ const rows = db.prepare("SELECT e.status status,e.unresolved_reason reason,e.evidence_json evidenceJson FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call'").all();
1372
+ const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
1373
+ const withoutOwnership = rows.filter((row) => row.reason === "local_service_candidate_without_caller_ownership" || String(row.evidenceJson ?? "").includes("local_service_candidate_without_caller_ownership")).length;
1374
+ const unresolved = rows.filter((row) => row.status === "unresolved").length;
1375
+ const outsideScope = rows.filter((row) => row.status === "unresolved" && candidateCount(row.evidenceJson) > 0).length;
1376
+ const out = [];
1377
+ 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}` });
1378
+ 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}` });
1379
+ if (strict && unresolved > 0) out.push({ severity: "warning", code: "local_service_calls_unresolved", message: `Unresolved local service calls: ${unresolved}` });
1380
+ 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}` });
1381
+ return out;
1382
+ }
1383
+ function parserQualityDiagnostics(db, strict) {
1384
+ if (!strict) return [];
1385
+ const symbol = symbolCallQuality(db);
1386
+ const dbq = dbQueryQuality(db);
1387
+ const outbound = outboundOwnershipQuality(db);
1388
+ return [
1389
+ identityAliasBindingQuality(db),
1390
+ remoteActionNoBindingQuality(db),
1391
+ contextualImplementationQuality(db),
1392
+ implementationCandidateQuality(db),
1393
+ classInstanceNoiseQuality(db),
1394
+ contextualBindingPropagationQuality(db),
1395
+ nestedThisReceiverQuality(db),
1396
+ wrapperPathPropagationQuality(db),
1397
+ remoteQueryTargetQuality(db),
1398
+ remoteEntityOperationCollisionQuality(db),
1399
+ remoteEntityDynamicOperationFalsePositiveQuality(db),
1400
+ remoteActionTargetQuality(db),
1401
+ externalHttpTargetQuality(db),
1402
+ odataInvocationResolutionQuality(db),
1403
+ ...jsonEvidenceQuality(db),
1404
+ eventReceiverQuality(db),
1405
+ graphDynamicFlagQuality(db),
1406
+ symbol,
1407
+ dbq,
1408
+ outbound
1409
+ ];
1410
+ }
1411
+ function jsonEvidenceQuality(db) {
1412
+ const symbol = 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();
1413
+ const outbound = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN evidence_json IS NULL THEN 1 ELSE 0 END) missing, SUM(CASE WHEN evidence_json IS NOT NULL AND json_valid(evidence_json)=0 THEN 1 ELSE 0 END) invalid, SUM(CASE WHEN evidence_json IS NOT NULL AND json_valid(evidence_json)=1 AND json_type(evidence_json) <> 'object' THEN 1 ELSE 0 END) nonObject FROM outbound_calls").get();
1414
+ const graph = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_type(e.evidence_json) <> 'object' THEN 1 ELSE 0 END) nonObject, SUM(CASE WHEN e.evidence_json IS NOT NULL AND json_valid(e.evidence_json)=1 AND json_extract(e.evidence_json,'$.outboundEvidence.parser') IS NOT NULL THEN 1 ELSE 0 END) withOutboundEvidence FROM graph_edges e WHERE e.from_kind='call'").get();
1415
+ const outboundExamples = db.prepare("SELECT call_type callType,source_file sourceFile,source_line sourceLine FROM outbound_calls WHERE evidence_json IS NULL OR json_valid(evidence_json)=0 OR json_type(evidence_json) <> 'object' ORDER BY source_file,source_line LIMIT 10").all();
1416
+ const graphExamples = db.prepare("SELECT c.call_type callType,c.source_file sourceFile,c.source_line sourceLine,e.edge_type edgeType FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_type(e.evidence_json) <> 'object' OR json_extract(e.evidence_json,'$.outboundEvidence.parser') IS NULL ORDER BY c.source_file,c.source_line LIMIT 10").all();
1417
+ return [
1418
+ { severity: Number(symbol.nonObject ?? 0) > 0 ? "warning" : "info", code: "strict_symbol_call_evidence_quality", message: "Symbol-call evidence JSON object aggregate", total: Number(symbol.total ?? 0), nonObject: Number(symbol.nonObject ?? 0) },
1419
+ { severity: Number(outbound.missing ?? 0) + Number(outbound.invalid ?? 0) + Number(outbound.nonObject ?? 0) > 0 ? "warning" : "info", code: "strict_outbound_evidence_quality", message: "Outbound parser evidence JSON object aggregate", total: Number(outbound.total ?? 0), missing: Number(outbound.missing ?? 0), invalid: Number(outbound.invalid ?? 0), nonObject: Number(outbound.nonObject ?? 0), examples: outboundExamples },
1420
+ { severity: Number(graph.nonObject ?? 0) > 0 || Number(graph.withOutboundEvidence ?? 0) < Number(graph.total ?? 0) ? "warning" : "info", code: "strict_graph_evidence_quality", message: "Call-derived graph evidence and parser-evidence propagation aggregate", total: Number(graph.total ?? 0), nonObject: Number(graph.nonObject ?? 0), withOutboundEvidence: Number(graph.withOutboundEvidence ?? 0), examples: graphExamples }
1421
+ ];
1422
+ }
1423
+ function symbolCallQuality(db) {
1424
+ const row = 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();
1425
+ 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();
1426
+ const total = Number(row.total ?? 0);
1427
+ const unresolved = Number(row.unresolved ?? 0);
1428
+ const ratio = total === 0 ? 0 : Number((unresolved / total).toFixed(4));
1429
+ return { severity: ratio > 0.05 ? "warning" : "info", code: "strict_symbol_call_quality", message: "Symbol-call quality aggregate", total, resolved: Number(row.resolved ?? 0), unresolved, unresolvedRatio: ratio, unresolvedRatioThreshold: 0.05, topUnresolvedCallees: top };
1430
+ }
1431
+ function dbQueryQuality(db) {
1432
+ const row = 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();
1433
+ const total = Number(row.total ?? 0);
1434
+ const unknown = Number(row.unknown ?? 0);
1435
+ const ratio = total === 0 ? 0 : Number((unknown / total).toFixed(4));
1436
+ return { severity: ratio > 0.25 ? "warning" : "info", code: "strict_db_query_quality", message: "Local DB query quality aggregate", total, known: Number(row.known ?? 0), unknown, unknownRatio: ratio, unknownRatioThreshold: 0.25 };
1437
+ }
1438
+ function outboundOwnershipQuality(db) {
1439
+ const row = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN source_symbol_id IS NULL THEN 1 ELSE 0 END) withoutOwnership FROM outbound_calls").get();
1440
+ const byType = 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();
1441
+ const examples = db.prepare("SELECT call_type callType,source_file sourceFile,source_line sourceLine,unresolved_reason unresolvedReason FROM outbound_calls WHERE source_symbol_id IS NULL ORDER BY source_file,source_line LIMIT 10").all();
1442
+ const total = Number(row.total ?? 0);
1443
+ const without = Number(row.withoutOwnership ?? 0);
1444
+ const ratio = total === 0 ? 0 : Number((without / total).toFixed(4));
1445
+ return { severity: ratio > 0.01 ? "warning" : "info", code: "strict_outbound_source_ownership_quality", message: "Outbound call source-symbol ownership aggregate", total, withoutOwnership: without, withoutOwnershipRatio: ratio, withoutOwnershipRatioThreshold: 0.01, ownerlessByType: byType, ownerlessExamples: examples };
1446
+ }
1447
+ function eventReceiverQuality(db) {
1448
+ const row = db.prepare("SELECT SUM(CASE WHEN call_type IN ('async_emit','async_subscribe') THEN 1 ELSE 0 END) eventTotal, SUM(CASE WHEN call_type IN ('async_emit','async_subscribe') AND (json_extract(evidence_json,'$.receiverClassification') IS NULL OR json_extract(evidence_json,'$.receiverClassification') <> 'cap_evidence') THEN 1 ELSE 0 END) questionable FROM outbound_calls").get();
1449
+ return { severity: Number(row.questionable ?? 0) > 0 ? "warning" : "info", code: "strict_event_receiver_classification_quality", message: "CAP event receiver classification aggregate", eventTotal: Number(row.eventTotal ?? 0), questionable: Number(row.questionable ?? 0) };
1450
+ }
1451
+ function graphDynamicFlagQuality(db) {
1452
+ const row = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get();
1453
+ return { severity: Number(row.count ?? 0) > 0 ? "warning" : "info", code: "strict_graph_dynamic_flag_consistency", message: "Graph dynamic flag consistency aggregate", dynamicTerminalEdges: Number(row.count ?? 0) };
1454
+ }
1455
+ function implementationCandidateQuality(db) {
1456
+ const categories = [...implementationEdgeCategories(db), missingParameterMetadataCategory(db)].filter((item) => item.count > 0);
1457
+ const total = categories.reduce((sum, item) => sum + item.count, 0);
1458
+ return { severity: total > 0 ? "warning" : "info", code: "strict_implementation_candidate_quality", message: "Implementation candidate ambiguity and rejection aggregate", total, categories };
1459
+ }
1460
+ function implementationEdgeCategories(db) {
1461
+ const rows = db.prepare(`SELECT e.status,e.unresolved_reason unresolvedReason,e.evidence_json evidenceJson,o.operation_name operationName,base.operation_name baseOperation,s.service_path servicePath
1462
+ FROM graph_edges e JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
1463
+ JOIN cds_services s ON s.id=o.service_id LEFT JOIN cds_operations base ON base.id=o.base_operation_id
1464
+ WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status IN ('ambiguous','unresolved') ORDER BY s.service_path,o.operation_name,e.id`).all();
1465
+ const grouped = /* @__PURE__ */ new Map();
1466
+ for (const row of rows) addImplementationCategory(grouped, row);
1467
+ return [...grouped.values()].map(({ servicePaths, ...item }) => ({ ...item, servicePathPattern: pathPattern(servicePaths), examples: item.examples.slice(0, 3) }));
1468
+ }
1469
+ function addImplementationCategory(grouped, row) {
1470
+ const evidence = parseObject(row.evidenceJson);
1471
+ const category = implementationCategory(row, evidence);
1472
+ const family = candidateFamily(evidence);
1473
+ const reason = category === "duplicate_package_name_candidates" ? "duplicate_package_name_candidates" : String(row.unresolvedReason ?? category);
1474
+ const baseOperation = String(row.baseOperation ?? row.operationName ?? evidence.operationName ?? "unknown");
1475
+ const key = [category, baseOperation, reason, family].join("\0");
1476
+ const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
1477
+ current.count += 1;
1478
+ current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ""));
1479
+ current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason });
1480
+ grouped.set(key, current);
1481
+ }
1482
+ function missingParameterMetadataCategory(db) {
1483
+ const examples = db.prepare(`SELECT sc.source_file sourceFile,sc.source_line sourceLine,sc.callee_expression calleeExpression
1484
+ FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1485
+ WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1486
+ AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)
1487
+ ORDER BY sc.source_file,sc.source_line LIMIT 3`).all();
1488
+ const row = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1489
+ WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1490
+ AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get();
1491
+ return { category: "missing_parameter_metadata", reason: "callee symbol is missing parameter binding metadata", candidateFamily: "symbol_parameter_metadata", count: Number(row.count ?? 0), examples };
1492
+ }
1493
+ function implementationCategory(row, evidence) {
1494
+ const reasons = JSON.stringify([evidence.ambiguityReasons, evidence.candidateFamilies, evidence.candidates, row.unresolvedReason]);
1495
+ if (reasons.includes("duplicate_package_name_candidates")) return "duplicate_package_name_candidates";
1496
+ if (reasons.includes("missing direct ownership")) return "missing_strong_ownership_evidence";
1497
+ return String(row.status) === "ambiguous" ? "ambiguous_implementation_candidates" : "rejected_implementation_candidates";
1498
+ }
1499
+ function candidateFamily(evidence) {
1500
+ const families = asRecords(evidence.candidateFamilies);
1501
+ const duplicate = families.find((row) => typeof row.packageName === "string");
1502
+ if (duplicate?.packageName) return String(duplicate.packageName);
1503
+ const candidates = asRecords(evidence.candidates);
1504
+ const first = candidates.find((row) => parseObject(row.handlerPackage).packageName);
1505
+ return String(parseObject(first?.handlerPackage).packageName ?? "unknown");
1506
+ }
1507
+ function pathPattern(paths) {
1508
+ const values = [...new Set(paths.filter(Boolean))].sort();
1509
+ if (values.length <= 1) return values[0] ?? "";
1510
+ const prefix = commonPrefix(values);
1511
+ const suffix = commonSuffix(values.map((value) => value.slice(prefix.length)));
1512
+ return `${prefix}*${suffix}`;
1513
+ }
1514
+ function commonPrefix(values) {
1515
+ let prefix = values[0] ?? "";
1516
+ for (const value of values.slice(1)) while (!value.startsWith(prefix)) prefix = prefix.slice(0, -1);
1517
+ return prefix;
1518
+ }
1519
+ function commonSuffix(values) {
1520
+ let suffix = values[0] ?? "";
1521
+ for (const value of values.slice(1)) while (!value.endsWith(suffix)) suffix = suffix.slice(1);
1522
+ return suffix;
1523
+ }
1524
+ function contextualImplementationQuality(db) {
1525
+ const rows = db.prepare("SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') GROUP BY status,reason ORDER BY status,count DESC,reason").all();
1526
+ const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1527
+ return { severity: total > 0 ? "warning" : "info", code: "strict_contextual_implementation_quality", message: "Implementation hops stopped by ambiguous or unresolved implementation edges", total, breakdown: rows };
1528
+ }
1529
+ function classInstanceNoiseQuality(db) {
1530
+ const builtIns = ["Set", "Map", "WeakSet", "WeakMap", "Date", "RegExp", "URL", "URLSearchParams", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "AggregateError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array", "Promise", "AbortController"];
1531
+ const placeholders = builtIns.map(() => "?").join(",");
1532
+ const aggregate = db.prepare(`SELECT COUNT(*) total,
1533
+ SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
1534
+ SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
1535
+ FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns);
1536
+ const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
1537
+ FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
1538
+ GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all();
1539
+ return { severity: Number(aggregate.unresolvedBuiltIn ?? 0) > 0 ? "warning" : "info", code: "strict_class_instance_noise_quality", message: "Class-instance symbol-call aggregate with built-in constructor guard", totalClassInstanceCalls: Number(aggregate.total ?? 0), unresolvedClassInstanceCalls: Number(aggregate.unresolved ?? 0), unresolvedBuiltInClassInstanceCalls: Number(aggregate.unresolvedBuiltIn ?? 0), unresolvedByConstructor: byConstructor };
1540
+ }
1541
+ function contextualBindingPropagationQuality(db) {
1542
+ const missing = missingParameterMetadataCategory(db);
1543
+ const opportunities = db.prepare("SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath FROM outbound_calls c WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL AND c.service_binding_id IS NULL ORDER BY c.source_file,c.source_line LIMIT 8").all();
1544
+ return { severity: missing.count + opportunities.length > 0 ? "warning" : "info", code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation opportunities for trace-time helper resolution", calleeSymbolsMissingParameterMetadata: missing.count, traceTimeContextualOpportunities: opportunities.length, examples: opportunities };
1545
+ }
1546
+ function wrapperPathPropagationQuality(db) {
1547
+ const examples = db.prepare("SELECT source_file sourceFile,source_line sourceLine,json_extract(evidence_json,'$.receiver') receiverName,json_extract(evidence_json,'$.operationPathExpression') pathIdentifier FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier' ORDER BY source_file,source_line LIMIT 5").all();
1548
+ return { severity: examples.length > 0 ? "warning" : "info", code: "strict_wrapper_path_propagation_quality", message: "Dynamic path sends where send({ path }) used a path identifier", dynamicPathIdentifierCalls: examples.length, examples };
1549
+ }
1550
+ function nestedThisReceiverQuality(db) {
1551
+ const aggregate = db.prepare(`SELECT COUNT(*) total,
1552
+ SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
1553
+ SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
1554
+ FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get();
1555
+ const examples = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,callee_expression calleeExpression,json_extract(evidence_json,'$.relation') relation,json_extract(evidence_json,'$.targetName') targetName
1556
+ FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
1557
+ ORDER BY source_file,source_line LIMIT 8`).all();
1558
+ return { severity: Number(aggregate.resolvedToCurrentClass ?? 0) > 0 ? "warning" : "info", code: "strict_nested_this_receiver_quality", message: "Nested this receiver symbol-call aggregate", nestedThisReceiverCallsConsidered: Number(aggregate.total ?? 0), nestedThisResolvedToCurrentClass: Number(aggregate.resolvedToCurrentClass ?? 0), nestedThisWithExplicitHelperInstanceEvidence: Number(aggregate.withExplicitHelperInstanceEvidence ?? 0), warningExamples: examples };
1559
+ }
1560
+ function remoteQueryTargetQuality(db) {
1561
+ const row = db.prepare("SELECT COUNT(*) total,SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.status='terminal' THEN 1 ELSE 0 END) terminal,SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,SUM(CASE WHEN e.edge_type='UNRESOLVED_EDGE' OR e.status='unresolved' THEN 1 ELSE 0 END) unresolved FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_query'").get();
1562
+ const numeric = Number(row.numericTargets ?? 0);
1563
+ const unresolved = Number(row.unresolved ?? 0);
1564
+ return { severity: numeric + unresolved > 0 ? "warning" : "info", code: "strict_remote_query_target_quality", message: "Remote query terminal target quality aggregate", totalRemoteQueryCalls: Number(row.total ?? 0), terminalRemoteQueryEdges: Number(row.terminal ?? 0), numericTargetCount: numeric, unresolvedRemoteQueryCount: unresolved };
1565
+ }
1566
+ function remoteEntityOperationCollisionQuality(db) {
1567
+ const rows = db.prepare(`SELECT c.id callId,c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,c.query_entity entitySegment,e.to_id selectedTerminalEntityTarget,e.evidence_json evidenceJson
1568
+ FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1569
+ WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
1570
+ ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1571
+ const examples = rows.map((row) => operationCollisionExample(db, row)).filter((row) => Boolean(row)).slice(0, 10);
1572
+ return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_operation_collision_quality", message: "Terminal remote entity edges that look like indexed operation invocations", collisionCount: examples.length, examples };
1573
+ }
1574
+ function operationCollisionExample(db, row) {
1575
+ const rawPath = String(row.rawPath ?? "");
1576
+ const normalized = normalizeODataOperationInvocationPath(rawPath);
1577
+ const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
1578
+ const name = candidatePath.replace(/^\//, "");
1579
+ const simple = name.split(".").at(-1) ?? name;
1580
+ const candidates = db.prepare("SELECT COUNT(*) count FROM cds_operations WHERE operation_path IN (?,?) OR operation_name IN (?,?)").get(candidatePath, `/${simple}`, name, simple);
1581
+ const candidateCount2 = Number(candidates.count ?? 0);
1582
+ if (!normalized?.wasInvocation && candidateCount2 === 0) return void 0;
1583
+ const evidence = parseObject(row.evidenceJson);
1584
+ return { callId: row.callId, sourceFile: row.sourceFile, sourceLine: row.sourceLine, method: row.method, rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : candidatePath, entitySegment: row.entitySegment, operationCandidateCount: candidateCount2, selectedTerminalEntityTarget: row.selectedTerminalEntityTarget, classifierReason: parseObject(evidence.odataPathIntent).reason };
1585
+ }
1586
+ function remoteEntityDynamicOperationFalsePositiveQuality(db) {
1587
+ const rows = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,e.id graphEdgeId,e.unresolved_reason unresolvedReason,e.evidence_json evidenceJson
1588
+ FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1589
+ WHERE c.call_type LIKE 'remote_entity_%' AND e.status IN ('dynamic','unresolved') AND e.to_kind='operation_candidate'
1590
+ ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1591
+ const examples = rows.filter(isRemoteEntityFalsePositive).map((row) => falsePositiveExample(row)).slice(0, 10);
1592
+ return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_dynamic_operation_false_positive_quality", message: "Parser-classified entity paths linked as dynamic operation candidates without indexed operation evidence", falsePositiveCount: examples.length, examples };
1593
+ }
1594
+ function isRemoteEntityFalsePositive(row) {
1595
+ const intent = classifyODataPathIntent(String(row.rawPath ?? ""), String(row.method ?? "GET"));
1596
+ const entityIntent = ["entity_key_read", "entity_navigation_query", "entity_media"].includes(intent.kind) || intent.kind === "entity_mutation" && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
1597
+ const evidence = parseObject(row.evidenceJson);
1598
+ const candidateCount2 = Number(evidence.indexedOperationCandidateCount ?? evidence.candidateCount ?? 0);
1599
+ const reason = String(row.unresolvedReason ?? "");
1600
+ return entityIntent && candidateCount2 === 0 && (intent.keyPredicatePlaceholderKeys.length > 0 || reason.includes("runtime variable") || reason.includes("placeholder"));
1601
+ }
1602
+ function falsePositiveExample(row) {
1603
+ const intent = classifyODataPathIntent(String(row.rawPath ?? ""), String(row.method ?? "GET"));
1604
+ return { sourceFile: row.sourceFile, sourceLine: row.sourceLine, rawPath: row.rawPath, method: row.method, pathIntent: intent.kind, keyPlaceholderKeys: intent.keyPredicatePlaceholderKeys, navigationOrMediaSuffix: intent.navigationSuffix ?? intent.mediaOrPropertySuffix, graphEdgeId: row.graphEdgeId, operationCandidateCount: 0, recommendedRemediation: "Reindex and relink with service-flow 0.1.35 or newer so entity key placeholders remain entity-addressing evidence." };
1605
+ }
1606
+ function remoteActionTargetQuality(db) {
1607
+ const row = db.prepare("SELECT COUNT(*) total,SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved,SUM(CASE WHEN e.status='unresolved' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,SUM(CASE WHEN e.status='unresolved' AND (e.to_id='Remote action: unknown path' OR e.to_id='Remote action: dynamic path') THEN 1 ELSE 0 END) semanticTargets FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action'").get();
1608
+ const numeric = Number(row.numericTargets ?? 0);
1609
+ return { severity: numeric > 0 ? "warning" : "info", code: "strict_remote_action_target_quality", message: "Remote action unresolved target quality aggregate", totalRemoteActionCalls: Number(row.total ?? 0), unresolvedRemoteActionCalls: Number(row.unresolved ?? 0), numericUnresolvedTargetCount: numeric, semanticUnknownOrDynamicTargetCount: Number(row.semanticTargets ?? 0) };
1610
+ }
1611
+ function externalHttpTargetQuality(db) {
1612
+ const row = db.prepare("SELECT COUNT(*) total,SUM(CASE WHEN e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,SUM(CASE WHEN e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_extract(e.evidence_json,'$.externalTarget.kind') IS NULL THEN 1 ELSE 0 END) invalidEvidence FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='external_http'").get();
1613
+ const bad = Number(row.numericTargets ?? 0) + Number(row.invalidEvidence ?? 0);
1614
+ return { severity: bad > 0 ? "warning" : "info", code: "strict_external_http_target_quality", message: "External HTTP semantic target aggregate", totalExternalHttpCalls: Number(row.total ?? 0), numericTargetCount: Number(row.numericTargets ?? 0), invalidOrMissingExternalTargetEvidence: Number(row.invalidEvidence ?? 0) };
1615
+ }
1616
+ function odataInvocationResolutionQuality(db) {
1617
+ const rows = db.prepare("SELECT c.operation_path_expr operationPathExpr,e.status status FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action' AND c.operation_path_expr LIKE '%(%'").all();
1618
+ const unresolved = rows.filter((row) => row.status === "unresolved" && normalizeODataOperationInvocationPath(row.operationPathExpr)?.wasInvocation).length;
1619
+ const ambiguous = rows.filter((row) => row.status === "ambiguous" && normalizeODataOperationInvocationPath(row.operationPathExpr)?.wasInvocation).length;
1620
+ return { severity: unresolved + ambiguous > 0 ? "warning" : "info", code: "strict_odata_invocation_resolution_quality", message: "OData invocation-path resolution quality aggregate", totalInvocationRemoteActions: rows.length, resolvedInvocationCalls: rows.filter((row) => row.status === "resolved").length, dynamicInvocationCalls: rows.filter((row) => row.status === "dynamic").length, ambiguousInvocationCalls: rows.filter((row) => row.status === "ambiguous").length, unresolvedInvocationCalls: rows.filter((row) => row.status === "unresolved").length, ambiguousNormalizedCalls: ambiguous, unresolvedNormalizedCallsWithIndexedCandidates: unresolved };
1621
+ }
1622
+ function identityAliasBindingQuality(db) {
1623
+ const examples = db.prepare("SELECT c.source_file sourceFile,c.source_line sourceLine FROM outbound_calls c WHERE c.call_type='remote_action' AND c.service_binding_id IS NULL AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL ORDER BY c.source_file,c.source_line LIMIT 5").all();
1624
+ return { severity: examples.length > 0 ? "warning" : "info", code: "strict_identity_alias_binding_quality", message: "Remote sends that look like missed same-file identity aliases", missedAliasBindingCalls: examples.length, examples };
1625
+ }
1626
+ function remoteActionNoBindingQuality(db) {
1627
+ const rows = db.prepare("SELECT COALESCE(e.status,'missing_edge') status,COUNT(*) count FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL GROUP BY status ORDER BY count DESC,status").all();
1628
+ const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1629
+ return { severity: total > 0 ? "warning" : "info", code: "strict_remote_action_no_binding_quality", message: "Remote actions with operation paths but no service binding id", total, breakdown: rows };
1630
+ }
1631
+ function candidateCount(value) {
1632
+ return Number(parseObject(value).candidateCount ?? 0);
1633
+ }
1634
+ function parseObject(value) {
1635
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
1636
+ try {
1637
+ const parsed = JSON.parse(String(value ?? "{}"));
1638
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1639
+ } catch {
1640
+ return {};
1641
+ }
1642
+ }
1643
+ function asRecords(value) {
1644
+ return Array.isArray(value) ? value.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : [];
1645
+ }
1646
+
1243
1647
  // src/trace/selectors.ts
1244
1648
  function parseVars(values) {
1245
1649
  const out = {};
@@ -1353,374 +1757,6 @@ async function withReadOnlyWorkspace(workspace, fn) {
1353
1757
  db.close();
1354
1758
  }
1355
1759
  }
1356
- function schemaDriftDiagnostics(db, strict) {
1357
- if (!strict) return [];
1358
- const symbolColumns = db.prepare("PRAGMA table_info(symbols)").all();
1359
- const legacy = symbolColumns.filter((row) => ["external_target_kind", "external_target_id", "external_target_label", "external_target_dynamic"].includes(String(row.name))).map((row) => row.name);
1360
- const missingExternal = db.prepare("SELECT id id,source_file sourceFile,source_line sourceLine FROM outbound_calls WHERE call_type='external_http' AND (external_target_id IS NULL OR external_target_label IS NULL OR external_target_kind IS NULL) LIMIT 20").all();
1361
- const diagnostics = [];
1362
- if (legacy.length > 0) diagnostics.push({ severity: "warning", code: "schema_legacy_columns_present", message: "Legacy external-target columns are present on symbols; run service-flow clean --db-only, then init/index/link to rebuild with the current schema.", scope: "workspace", affectedColumns: legacy, remediation: "service-flow clean --db-only && service-flow init <workspace> && service-flow index && service-flow link" });
1363
- if (missingExternal.length > 0) diagnostics.push({ severity: "warning", code: "external_target_columns_missing_data", message: "External HTTP calls are missing queryable external target metadata; reindex is required after upgrade.", scope: "workspace", affectedRows: missingExternal, remediation: "service-flow index --force && service-flow link" });
1364
- if (legacy.length > 0 || missingExternal.length > 0) diagnostics.push({ severity: "warning", code: "reindex_required_after_upgrade", message: "This database cannot be made equivalent to a fresh index by relink alone.", scope: "workspace", remediation: "Rebuild or force reindex the workspace, then run service-flow doctor --strict." });
1365
- return diagnostics;
1366
- }
1367
- function linkUpgradeWarnings(db) {
1368
- return [...schemaDriftDiagnostics(db, true), ...analyzerVersionDiagnostics(db, true)].filter((item) => ["schema_legacy_columns_present", "external_target_columns_missing_data", "reindex_required_after_upgrade", "reindex_required_after_analyzer_upgrade"].includes(String(item.code)));
1369
- }
1370
- function analyzerVersionDiagnostics(db, strict) {
1371
- if (!strict) return [];
1372
- const rows = db.prepare("SELECT name,COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion FROM repositories WHERE index_status='indexed' AND COALESCE(fact_analyzer_version,'legacy')<>?").all(ANALYZER_VERSION);
1373
- if (rows.length === 0) return [];
1374
- return [{ severity: "warning", code: "reindex_required_after_analyzer_upgrade", message: "Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.", scope: "workspace", affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: "service-flow index --force && service-flow link" }];
1375
- }
1376
- function remoteEntityOperationCollisionQuality(db) {
1377
- const rows = db.prepare(`SELECT c.id callId,c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,c.query_entity entitySegment,e.to_id selectedTerminalEntityTarget,e.evidence_json evidenceJson
1378
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1379
- WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
1380
- ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1381
- const examples = [];
1382
- for (const row of rows) {
1383
- const normalized = normalizeODataOperationInvocationPath(String(row.rawPath ?? ""));
1384
- const rawPath = String(row.rawPath ?? "");
1385
- const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
1386
- const name = candidatePath.replace(/^\//, "");
1387
- const simple = name.split(".").at(-1) ?? name;
1388
- const candidates = db.prepare("SELECT COUNT(*) count FROM cds_operations WHERE operation_path IN (?,?) OR operation_name IN (?,?)").get(candidatePath, `/${simple}`, name, simple);
1389
- const candidateCount = Number(candidates.count ?? 0);
1390
- const operationLike = Boolean(normalized?.wasInvocation) || candidateCount > 0;
1391
- if (!operationLike) continue;
1392
- let classifierReason;
1393
- try {
1394
- const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1395
- classifierReason = evidence.odataPathIntent?.reason;
1396
- } catch {
1397
- classifierReason = void 0;
1398
- }
1399
- examples.push({ callId: row.callId, sourceFile: row.sourceFile, sourceLine: row.sourceLine, method: row.method, rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : candidatePath, entitySegment: row.entitySegment, operationCandidateCount: candidateCount, selectedTerminalEntityTarget: row.selectedTerminalEntityTarget, classifierReason });
1400
- }
1401
- return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_operation_collision_quality", message: "Terminal remote entity edges that look like indexed operation invocations", collisionCount: examples.length, examples: examples.slice(0, 10) };
1402
- }
1403
- function remoteEntityDynamicOperationFalsePositiveQuality(db) {
1404
- const rows = db.prepare(`SELECT c.id callId,c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,e.id graphEdgeId,e.status status,e.to_kind targetKind,e.unresolved_reason unresolvedReason,e.evidence_json evidenceJson
1405
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1406
- WHERE c.call_type LIKE 'remote_entity_%' AND e.status IN ('dynamic','unresolved') AND e.to_kind='operation_candidate'
1407
- ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1408
- const examples = [];
1409
- for (const row of rows) {
1410
- const rawPath = String(row.rawPath ?? "");
1411
- const method = String(row.method ?? "GET");
1412
- const intent = classifyODataPathIntent(rawPath, method);
1413
- const entityIntent = ["entity_key_read", "entity_navigation_query", "entity_media"].includes(intent.kind) || intent.kind === "entity_mutation" && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
1414
- if (!entityIntent) continue;
1415
- let candidateCount;
1416
- try {
1417
- const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1418
- candidateCount = Number(evidence.indexedOperationCandidateCount ?? evidence.candidateCount ?? 0);
1419
- } catch {
1420
- candidateCount = 0;
1421
- }
1422
- const reason = String(row.unresolvedReason ?? "");
1423
- const keyEvidence = intent.keyPredicatePlaceholderKeys.length > 0 || reason.includes("runtime variable") || reason.includes("placeholder");
1424
- if (candidateCount > 0 || !keyEvidence) continue;
1425
- examples.push({ sourceFile: row.sourceFile, sourceLine: row.sourceLine, rawPath, method, pathIntent: intent.kind, keyPlaceholderKeys: intent.keyPredicatePlaceholderKeys, navigationOrMediaSuffix: intent.navigationSuffix ?? intent.mediaOrPropertySuffix, operationCandidateCount: candidateCount, graphEdgeId: row.graphEdgeId, recommendedRemediation: "Reindex and relink with service-flow 0.1.35 or newer so entity key placeholders remain entity-addressing evidence." });
1426
- }
1427
- return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_dynamic_operation_false_positive_quality", message: "Parser-classified entity paths linked as dynamic operation candidates without indexed operation evidence", falsePositiveCount: examples.length, examples: examples.slice(0, 10) };
1428
- }
1429
- function localServiceDiagnostics(db, strict) {
1430
- const rows = db.prepare(`SELECT e.status status,e.unresolved_reason reason,e.evidence_json evidenceJson FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call'`).all();
1431
- const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
1432
- const withoutOwnership = rows.filter((row) => row.reason === "local_service_candidate_without_caller_ownership" || String(row.evidenceJson ?? "").includes("local_service_candidate_without_caller_ownership")).length;
1433
- const unresolved = rows.filter((row) => row.status === "unresolved").length;
1434
- const outsideScope = rows.filter((row) => {
1435
- if (row.status !== "unresolved") return false;
1436
- try {
1437
- const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1438
- return Number(evidence.candidateCount ?? 0) > 0;
1439
- } catch {
1440
- return false;
1441
- }
1442
- }).length;
1443
- const out = [];
1444
- 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}` });
1445
- 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}` });
1446
- if (strict && unresolved > 0) out.push({ severity: "warning", code: "local_service_calls_unresolved", message: `Unresolved local service calls: ${unresolved}` });
1447
- 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}` });
1448
- return out;
1449
- }
1450
- function parserQualityDiagnostics(db, strict) {
1451
- if (!strict) return [];
1452
- const symbolUnresolvedThreshold = 0.05;
1453
- const dbUnknownThreshold = 0.25;
1454
- const outboundUnownedThreshold = 0.01;
1455
- 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();
1456
- 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();
1457
- 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();
1458
- 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();
1459
- 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();
1460
- const outboundEvidence = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN evidence_json IS NULL THEN 1 ELSE 0 END) missing, SUM(CASE WHEN evidence_json IS NOT NULL AND json_valid(evidence_json)=0 THEN 1 ELSE 0 END) invalid, SUM(CASE WHEN evidence_json IS NOT NULL AND json_valid(evidence_json)=1 AND json_type(evidence_json) <> 'object' THEN 1 ELSE 0 END) nonObject FROM outbound_calls").get();
1461
- const outboundEvidenceExamples = db.prepare("SELECT call_type callType, source_file sourceFile, source_line sourceLine FROM outbound_calls WHERE evidence_json IS NULL OR json_valid(evidence_json)=0 OR json_type(evidence_json) <> 'object' ORDER BY source_file, source_line LIMIT 10").all();
1462
- const graphEvidence = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_type(e.evidence_json) <> 'object' THEN 1 ELSE 0 END) nonObject, SUM(CASE WHEN e.evidence_json IS NOT NULL AND json_valid(e.evidence_json)=1 AND json_extract(e.evidence_json,'$.outboundEvidence.parser') IS NOT NULL THEN 1 ELSE 0 END) withOutboundEvidence FROM graph_edges e WHERE e.from_kind='call'").get();
1463
- const graphEvidenceExamples = db.prepare("SELECT c.call_type callType,c.source_file sourceFile,c.source_line sourceLine,e.edge_type edgeType FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_type(e.evidence_json) <> 'object' OR json_extract(e.evidence_json,'$.outboundEvidence.parser') IS NULL ORDER BY c.source_file,c.source_line LIMIT 10").all();
1464
- const eventReceiver = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN call_type IN ('async_emit','async_subscribe') THEN 1 ELSE 0 END) eventTotal, SUM(CASE WHEN call_type IN ('async_emit','async_subscribe') AND (json_extract(evidence_json,'$.receiverClassification') IS NULL OR json_extract(evidence_json,'$.receiverClassification') <> 'cap_evidence') THEN 1 ELSE 0 END) questionable FROM outbound_calls").get();
1465
- const dynamicTerminal = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get();
1466
- 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();
1467
- const ownerlessByCategory = db.prepare(`SELECT CASE
1468
- WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
1469
- WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
1470
- WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
1471
- 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'
1472
- WHEN source_line <= 1 THEN 'unsupported_function_shape'
1473
- WHEN source_line > 1 THEN 'unsupported_callback_shape'
1474
- ELSE 'unknown' END category, COUNT(*) count
1475
- FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY category ORDER BY count DESC, category`).all();
1476
- const ownerlessExamples = db.prepare(`SELECT CASE
1477
- WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
1478
- WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
1479
- WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
1480
- 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'
1481
- WHEN source_line <= 1 THEN 'unsupported_function_shape'
1482
- WHEN source_line > 1 THEN 'unsupported_callback_shape'
1483
- ELSE 'unknown' END category, call_type callType, source_file sourceFile, source_line sourceLine, unresolved_reason unresolvedReason
1484
- FROM outbound_calls WHERE source_symbol_id IS NULL ORDER BY category, source_file, source_line LIMIT 10`).all();
1485
- const symbolTotal = Number(symbol.total ?? 0);
1486
- const symbolUnresolved = Number(symbol.unresolved ?? 0);
1487
- const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
1488
- const queryTotal = Number(dbq.total ?? 0);
1489
- const queryUnknown = Number(dbq.unknown ?? 0);
1490
- const queryUnknownRatio = queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4));
1491
- const outboundTotal = Number(outbound.total ?? 0);
1492
- const outboundWithoutOwnership = Number(outbound.withoutOwnership ?? 0);
1493
- const outboundWithoutOwnershipRatio = outboundTotal === 0 ? 0 : Number((outboundWithoutOwnership / outboundTotal).toFixed(4));
1494
- const remoteQuery = remoteQueryTargetQuality(db);
1495
- const invocation = odataInvocationResolutionQuality(db);
1496
- const remoteAction = remoteActionTargetQuality(db);
1497
- const entityOperationCollision = remoteEntityOperationCollisionQuality(db);
1498
- const entityDynamicFalsePositive = remoteEntityDynamicOperationFalsePositiveQuality(db);
1499
- const externalHttp = externalHttpTargetQuality(db);
1500
- const aliasQuality = identityAliasBindingQuality(db);
1501
- const noBindingQuality = remoteActionNoBindingQuality(db);
1502
- const contextualQuality = contextualImplementationQuality(db);
1503
- const classInstanceQuality = classInstanceNoiseQuality(db);
1504
- const bindingPropagationQuality = contextualBindingPropagationQuality(db);
1505
- const wrapperQuality = wrapperPathPropagationQuality(db);
1506
- const nestedThisQuality = nestedThisReceiverQuality(db);
1507
- return [
1508
- aliasQuality,
1509
- noBindingQuality,
1510
- contextualQuality,
1511
- classInstanceQuality,
1512
- bindingPropagationQuality,
1513
- wrapperQuality,
1514
- nestedThisQuality,
1515
- remoteQuery,
1516
- entityOperationCollision,
1517
- entityDynamicFalsePositive,
1518
- invocation,
1519
- remoteAction,
1520
- externalHttp,
1521
- { 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) },
1522
- { severity: Number(outboundEvidence.missing ?? 0) + Number(outboundEvidence.invalid ?? 0) + Number(outboundEvidence.nonObject ?? 0) > 0 ? "warning" : "info", code: "strict_outbound_evidence_quality", message: "Outbound parser evidence JSON object aggregate", total: Number(outboundEvidence.total ?? 0), missing: Number(outboundEvidence.missing ?? 0), invalid: Number(outboundEvidence.invalid ?? 0), nonObject: Number(outboundEvidence.nonObject ?? 0), examples: outboundEvidenceExamples },
1523
- { severity: Number(graphEvidence.nonObject ?? 0) > 0 || Number(graphEvidence.withOutboundEvidence ?? 0) < Number(graphEvidence.total ?? 0) ? "warning" : "info", code: "strict_graph_evidence_quality", message: "Call-derived graph evidence and parser-evidence propagation aggregate", total: Number(graphEvidence.total ?? 0), nonObject: Number(graphEvidence.nonObject ?? 0), withOutboundEvidence: Number(graphEvidence.withOutboundEvidence ?? 0), examples: graphEvidenceExamples },
1524
- { severity: Number(eventReceiver.questionable ?? 0) > 0 ? "warning" : "info", code: "strict_event_receiver_classification_quality", message: "CAP event receiver classification aggregate", eventTotal: Number(eventReceiver.eventTotal ?? 0), questionable: Number(eventReceiver.questionable ?? 0) },
1525
- { severity: Number(dynamicTerminal.count ?? 0) > 0 ? "warning" : "info", code: "strict_graph_dynamic_flag_consistency", message: "Graph dynamic flag consistency aggregate", dynamicTerminalEdges: Number(dynamicTerminal.count ?? 0) },
1526
- { 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 },
1527
- { 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 },
1528
- { 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 }
1529
- ];
1530
- }
1531
- function identityAliasBindingQuality(db) {
1532
- const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,c.service_binding_id serviceBindingId,json_extract(c.evidence_json,'$.receiver') receiverName,b.variable_name aliasSourceVariable,'same-file identifier alias still lacks a binding id' parserReason
1533
- FROM outbound_calls c JOIN service_bindings b ON b.repo_id=c.repo_id AND b.source_file=c.source_file
1534
- WHERE c.call_type='remote_action' AND c.service_binding_id IS NULL AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1535
- AND c.evidence_json LIKE '%' || '"aliasOf":"' || json_extract(c.evidence_json,'$.receiver') || '"' || '%'
1536
- ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1537
- return { severity: examples.length > 0 ? "warning" : "info", code: "strict_identity_alias_binding_quality", message: "Remote sends that look like missed same-file identity aliases", missedAliasBindingCalls: examples.length, examples };
1538
- }
1539
- function remoteActionNoBindingQuality(db) {
1540
- const categoryCase = `CASE
1541
- WHEN c.unresolved_reason='dynamic_operation_path_identifier' THEN 'dynamic_path_identifier'
1542
- WHEN json_extract(c.evidence_json,'$.classifier')='higher_order_wrapper_literal_path' OR json_extract(c.evidence_json,'$.operationPathExpression') IS NOT NULL THEN 'likely_higher_order_wrapper_path_needed'
1543
- WHEN json_extract(c.evidence_json,'$.receiver') LIKE '%.%' THEN 'likely_parameter_context_needed'
1544
- WHEN EXISTS (
1545
- SELECT 1 FROM symbol_calls sc
1546
- JOIN symbols caller ON caller.id=sc.caller_symbol_id
1547
- JOIN symbols callee ON callee.id=sc.callee_symbol_id
1548
- WHERE sc.status='resolved'
1549
- AND sc.source_file=c.source_file
1550
- AND caller.id=c.source_symbol_id
1551
- AND json_extract(sc.evidence_json,'$.relation')='class_instance_method'
1552
- AND (callee.evidence_json IS NULL OR json_extract(callee.evidence_json,'$.parameterBindings') IS NULL)
1553
- ) THEN 'likely_instance_method_parameter_metadata_needed'
1554
- WHEN EXISTS (SELECT 1 FROM service_bindings b WHERE b.repo_id=c.repo_id AND b.source_file=c.source_file AND ABS(b.source_line-c.source_line) < 50) THEN 'likely_missing_assignment_binding'
1555
- WHEN e.status='unresolved' AND COALESCE(e.unresolved_reason,'') LIKE '%No indexed target operation%' THEN 'no_indexed_target_operation'
1556
- WHEN c.operation_path_expr IS NOT NULL AND (c.operation_path_expr LIKE '/%' OR c.operation_path_expr NOT LIKE '%/%') THEN 'operation_path_only_no_static_service_signal'
1557
- ELSE 'external_or_entity_path_not_action' END`;
1558
- const rows = db.prepare(`SELECT ${categoryCase} category,COALESCE(e.status,'missing_edge') status,COUNT(*) count
1559
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1560
- WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL
1561
- GROUP BY category,status ORDER BY count DESC,category,status`).all();
1562
- const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath,COALESCE(e.status,'missing_edge') status,${categoryCase} category
1563
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1564
- WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL ORDER BY c.source_file,c.source_line LIMIT 8`).all();
1565
- const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1566
- return { severity: total > 0 ? "warning" : "info", code: "strict_remote_action_no_binding_quality", message: "Remote actions with operation paths but no service binding id", total, breakdown: rows, examples };
1567
- }
1568
- function classInstanceNoiseQuality(db) {
1569
- const builtIns = ["Set", "Map", "WeakSet", "WeakMap", "Date", "RegExp", "URL", "URLSearchParams", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "AggregateError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array", "Promise", "AbortController"];
1570
- const placeholders = builtIns.map(() => "?").join(",");
1571
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1572
- SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
1573
- SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
1574
- FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns);
1575
- const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
1576
- FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
1577
- GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all();
1578
- return { severity: Number(aggregate.unresolvedBuiltIn ?? 0) > 0 ? "warning" : "info", code: "strict_class_instance_noise_quality", message: "Class-instance symbol-call aggregate with built-in constructor guard", totalClassInstanceCalls: Number(aggregate.total ?? 0), unresolvedClassInstanceCalls: Number(aggregate.unresolved ?? 0), unresolvedBuiltInClassInstanceCalls: Number(aggregate.unresolvedBuiltIn ?? 0), unresolvedByConstructor: byConstructor };
1579
- }
1580
- function contextualBindingPropagationQuality(db) {
1581
- const serviceClientCalls = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc
1582
- WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')`).get();
1583
- const missingMetadata = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1584
- WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1585
- AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get();
1586
- const destructuredUnmapped = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1587
- WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind')='object_literal'
1588
- AND json_extract(s.evidence_json,'$.parameterBindings[0].kind')='object_pattern'
1589
- AND json_array_length(json_extract(sc.evidence_json,'$.callArguments[0].properties')) > json_array_length(json_extract(s.evidence_json,'$.parameterBindings[0].properties'))`).get();
1590
- const opportunities = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath,b.alias bindingAlias,b.alias_expr bindingAliasExpr,b.service_path_expr servicePathExpr,b.destination_expr destinationExpr,req.service_path requireServicePath,req.destination requireDestination,COALESCE(e.status,'missing_edge') persistedStatus,
1591
- CASE
1592
- WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
1593
- WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
1594
- WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
1595
- ELSE 'trace_time_contextual_binding_candidate'
1596
- END contextualStatus
1597
- FROM outbound_calls c
1598
- LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1599
- LEFT JOIN service_bindings b ON b.id=c.service_binding_id
1600
- LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
1601
- WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1602
- AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
1603
- AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1604
- ORDER BY c.source_file,c.source_line LIMIT 8`).all();
1605
- const statusRows = db.prepare(`SELECT contextualStatus,COUNT(*) count FROM (
1606
- SELECT CASE
1607
- WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
1608
- WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
1609
- WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
1610
- ELSE 'trace_time_contextual_binding_candidate'
1611
- END contextualStatus
1612
- FROM outbound_calls c
1613
- LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1614
- LEFT JOIN service_bindings b ON b.id=c.service_binding_id
1615
- LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
1616
- WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1617
- AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
1618
- AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1619
- ) GROUP BY contextualStatus ORDER BY count DESC,contextualStatus`).all();
1620
- const resolvedContextual = db.prepare(`SELECT COUNT(*) count FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action' AND e.status='resolved' AND c.service_binding_id IS NOT NULL`).get();
1621
- const totalOpportunities = statusRows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1622
- const actionableStatuses = /* @__PURE__ */ new Set(["alias_without_matching_cds_requires", "cds_requires_present_but_persisted_resolution_unresolved", "trace_time_contextual_binding_candidate"]);
1623
- const actionableOpportunityCount = statusRows.reduce((sum, row) => actionableStatuses.has(String(row.contextualStatus)) ? sum + Number(row.count ?? 0) : sum, 0);
1624
- const severity = Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) + actionableOpportunityCount > 0 ? "warning" : "info";
1625
- return { severity, code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation opportunities for trace-time helper resolution", localSymbolCallsWithServiceClientArguments: Number(serviceClientCalls.count ?? 0), calleeSymbolsMissingParameterMetadata: Number(missingMetadata.count ?? 0), destructuredObjectParametersPossiblyUnmapped: Number(destructuredUnmapped.count ?? 0), contextualHelperSendsResolvedDuringPersistedLink: Number(resolvedContextual.count ?? 0), traceTimeContextualOpportunities: totalOpportunities, traceTimeContextualOpportunityBreakdown: statusRows.length > 0 ? statusRows : [{ contextualStatus: "no_contextual_opportunity", count: 0 }], exampleCount: opportunities.length, examples: opportunities };
1626
- }
1627
- function nestedThisReceiverQuality(db) {
1628
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1629
- SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
1630
- SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
1631
- FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get();
1632
- const examples = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,callee_expression calleeExpression,json_extract(evidence_json,'$.relation') relation,json_extract(evidence_json,'$.targetName') targetName
1633
- FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
1634
- ORDER BY source_file,source_line LIMIT 8`).all();
1635
- return { severity: Number(aggregate.resolvedToCurrentClass ?? 0) > 0 ? "warning" : "info", code: "strict_nested_this_receiver_quality", message: "Nested this receiver symbol-call aggregate", nestedThisReceiverCallsConsidered: Number(aggregate.total ?? 0), nestedThisResolvedToCurrentClass: Number(aggregate.resolvedToCurrentClass ?? 0), nestedThisWithExplicitHelperInstanceEvidence: Number(aggregate.withExplicitHelperInstanceEvidence ?? 0), warningExamples: examples };
1636
- }
1637
- function contextualImplementationQuality(db) {
1638
- const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count
1639
- FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') GROUP BY status,reason ORDER BY status,count DESC,reason`).all();
1640
- const examples = db.prepare(`SELECT json_extract(evidence_json,'$.servicePath') servicePath,json_extract(evidence_json,'$.operationPath') operationPath,status,unresolved_reason unresolvedReason,
1641
- json_extract(evidence_json,'$.candidates[0].rejectedReasons[0]') topRejectedReason,
1642
- json_extract(evidence_json,'$.candidates[0].acceptedReasons[0]') topAcceptedReason
1643
- FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') ORDER BY status,id LIMIT 6`).all();
1644
- const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1645
- return { severity: total > 0 ? "warning" : "info", code: "strict_contextual_implementation_quality", message: "Implementation hops stopped by ambiguous or unresolved implementation edges", total, breakdown: rows, examples };
1646
- }
1647
- function wrapperPathPropagationQuality(db) {
1648
- const examples = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,json_extract(evidence_json,'$.receiver') receiverName,json_extract(evidence_json,'$.operationPathExpression') pathIdentifier,CASE WHEN json_extract(evidence_json,'$.literalCallerArgumentDetected') IS NOT NULL THEN 1 ELSE 0 END literalCallerArgumentDetected
1649
- FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier' ORDER BY source_file,source_line LIMIT 5`).all();
1650
- const aggregate = db.prepare("SELECT COUNT(*) count FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier'").get();
1651
- return { severity: Number(aggregate.count ?? 0) > 0 ? "warning" : "info", code: "strict_wrapper_path_propagation_quality", message: "Dynamic path sends where send({ path }) used a path identifier", dynamicPathIdentifierCalls: Number(aggregate.count ?? 0), examples };
1652
- }
1653
- function remoteQueryTargetQuality(db) {
1654
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1655
- SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.status='terminal' THEN 1 ELSE 0 END) terminal,
1656
- SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
1657
- SUM(CASE WHEN e.edge_type='UNRESOLVED_EDGE' OR e.status='unresolved' THEN 1 ELSE 0 END) unresolved
1658
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_query'`).get();
1659
- const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,c.query_entity queryEntity,e.edge_type edgeType,e.status status,e.to_id target
1660
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1661
- WHERE c.call_type='remote_query' AND (e.id IS NULL OR e.edge_type<>'HANDLER_RUNS_REMOTE_QUERY' OR e.status<>'terminal' OR e.to_id GLOB '[0-9]*')
1662
- ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1663
- const numericTargets = Number(aggregate.numericTargets ?? 0);
1664
- const unresolved = Number(aggregate.unresolved ?? 0);
1665
- return { severity: numericTargets + unresolved > 0 ? "warning" : "info", code: "strict_remote_query_target_quality", message: "Remote query terminal target quality aggregate", totalRemoteQueryCalls: Number(aggregate.total ?? 0), terminalRemoteQueryEdges: Number(aggregate.terminal ?? 0), numericTargetCount: numericTargets, unresolvedRemoteQueryCount: unresolved, examples };
1666
- }
1667
- function remoteActionTargetQuality(db) {
1668
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1669
- SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved,
1670
- SUM(CASE WHEN e.status='unresolved' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
1671
- SUM(CASE WHEN e.status='unresolved' AND (e.to_id='Remote action: unknown path' OR e.to_id='Remote action: dynamic path') THEN 1 ELSE 0 END) semanticTargets
1672
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='remote_action'`).get();
1673
- const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,c.operation_path_expr operationPath,e.status status,e.to_id target
1674
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1675
- WHERE c.call_type='remote_action' AND e.status='unresolved' AND e.to_id GLOB '[0-9]*' ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1676
- const numericTargets = Number(aggregate.numericTargets ?? 0);
1677
- return { severity: numericTargets > 0 ? "warning" : "info", code: "strict_remote_action_target_quality", message: "Remote action unresolved target quality aggregate", totalRemoteActionCalls: Number(aggregate.total ?? 0), unresolvedRemoteActionCalls: Number(aggregate.unresolved ?? 0), numericUnresolvedTargetCount: numericTargets, semanticUnknownOrDynamicTargetCount: Number(aggregate.semanticTargets ?? 0), examples };
1678
- }
1679
- function externalHttpTargetQuality(db) {
1680
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1681
- SUM(CASE WHEN e.to_kind='external_destination' THEN 1 ELSE 0 END) destinationTargets,
1682
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.kind')='static_url' THEN 1 ELSE 0 END) staticEndpointTargets,
1683
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.dynamic')=1 THEN 1 ELSE 0 END) dynamicEndpointTargets,
1684
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.kind')='unknown' THEN 1 ELSE 0 END) unknownEndpointTargets,
1685
- SUM(CASE WHEN e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
1686
- SUM(CASE WHEN e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_extract(e.evidence_json,'$.externalTarget.kind') IS NULL THEN 1 ELSE 0 END) invalidEvidence
1687
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT) WHERE c.call_type='external_http'`).get();
1688
- const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,e.to_kind targetKind,e.to_id targetId,json_extract(e.evidence_json,'$.externalTarget.label') label,json_extract(e.evidence_json,'$.externalTarget.kind') kind
1689
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1690
- WHERE c.call_type='external_http' AND (e.to_id GLOB '[0-9]*' OR e.evidence_json IS NULL OR json_valid(e.evidence_json)=0 OR json_extract(e.evidence_json,'$.externalTarget.kind') IS NULL)
1691
- ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1692
- const numericTargets = Number(aggregate.numericTargets ?? 0);
1693
- const invalidEvidence = Number(aggregate.invalidEvidence ?? 0);
1694
- return { severity: numericTargets + invalidEvidence > 0 ? "warning" : "info", code: "strict_external_http_target_quality", message: "External HTTP semantic target aggregate", totalExternalHttpCalls: Number(aggregate.total ?? 0), semanticDestinationTargets: Number(aggregate.destinationTargets ?? 0), semanticStaticEndpointTargets: Number(aggregate.staticEndpointTargets ?? 0), dynamicEndpointTargets: Number(aggregate.dynamicEndpointTargets ?? 0), unknownEndpointTargets: Number(aggregate.unknownEndpointTargets ?? 0), numericTargetCount: numericTargets, invalidOrMissingExternalTargetEvidence: invalidEvidence, examples };
1695
- }
1696
- function odataInvocationResolutionQuality(db) {
1697
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1698
- SUM(CASE WHEN e.status='resolved' THEN 1 ELSE 0 END) resolved,
1699
- SUM(CASE WHEN e.status='dynamic' THEN 1 ELSE 0 END) dynamic,
1700
- SUM(CASE WHEN e.status='ambiguous' THEN 1 ELSE 0 END) ambiguous,
1701
- SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved
1702
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1703
- WHERE c.call_type='remote_action' AND c.operation_path_expr LIKE '%(%'`).get();
1704
- const rows = db.prepare(`SELECT c.id id,c.operation_path_expr operationPathExpr,c.source_file sourceFile,c.source_line sourceLine,e.status status,e.unresolved_reason unresolvedReason
1705
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1706
- WHERE c.call_type='remote_action' AND e.status IN ('unresolved','ambiguous') AND c.operation_path_expr LIKE '%(%'
1707
- ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1708
- const examples = [];
1709
- let unresolvedMatchingIndexedOperation = 0;
1710
- let ambiguousNormalizedCalls = 0;
1711
- for (const row of rows) {
1712
- const normalized = normalizeODataOperationInvocationPath(row.operationPathExpr);
1713
- if (!normalized?.wasInvocation) continue;
1714
- const normalizedName = normalized.normalizedOperationPath.replace(/^\//, "");
1715
- const simpleName = normalizedName.split(".").at(-1) ?? normalizedName;
1716
- const candidates = db.prepare("SELECT s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.operation_path IN (?,?) OR o.operation_name IN (?,?) ORDER BY s.service_path,o.operation_name LIMIT 5").all(normalized.normalizedOperationPath, `/${simpleName}`, normalizedName, simpleName);
1717
- if (candidates.length === 0) continue;
1718
- if (row.status === "ambiguous") ambiguousNormalizedCalls += 1;
1719
- if (row.status === "unresolved") unresolvedMatchingIndexedOperation += 1;
1720
- if (examples.length < 5) examples.push({ sourceFile: row.sourceFile, sourceLine: row.sourceLine, rawOperationPath: row.operationPathExpr, normalizedOperationPath: normalized.normalizedOperationPath, candidateCount: candidates.length, candidates });
1721
- }
1722
- return { severity: unresolvedMatchingIndexedOperation + ambiguousNormalizedCalls > 0 ? "warning" : "info", code: "strict_odata_invocation_resolution_quality", message: "OData invocation-path resolution quality aggregate", totalInvocationRemoteActions: Number(aggregate.total ?? 0), resolvedInvocationCalls: Number(aggregate.resolved ?? 0), dynamicInvocationCalls: Number(aggregate.dynamic ?? 0), ambiguousInvocationCalls: Number(aggregate.ambiguous ?? 0), unresolvedInvocationCalls: Number(aggregate.unresolved ?? 0), ambiguousNormalizedCalls, unresolvedNormalizedCallsWithIndexedCandidates: unresolvedMatchingIndexedOperation, examples };
1723
- }
1724
1760
  function createProgram() {
1725
1761
  const program = new Command();
1726
1762
  program.name("service-flow").description(
@@ -1752,7 +1788,7 @@ function createProgram() {
1752
1788
  );
1753
1789
  }).catch(fail)
1754
1790
  );
1755
- program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").option("--format <format>", "table|json|mermaid", "table").option("--include-external").option("--include-db").option("--include-async").option("--var <key=value>", "dynamic variable", collect, []).action(
1791
+ program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").option("--format <format>", "table|json|mermaid", "table").option("--include-external").option("--include-db").option("--include-async").option("--implementation-repo <name>").option("--var <key=value>", "dynamic variable", collect, []).action(
1756
1792
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1757
1793
  const result = trace(
1758
1794
  db,
@@ -1768,7 +1804,8 @@ function createProgram() {
1768
1804
  vars: parseVars(opts.var),
1769
1805
  includeExternal: Boolean(opts.includeExternal),
1770
1806
  includeDb: Boolean(opts.includeDb),
1771
- includeAsync: Boolean(opts.includeAsync)
1807
+ includeAsync: Boolean(opts.includeAsync),
1808
+ implementationRepo: opts.implementationRepo
1772
1809
  }
1773
1810
  );
1774
1811
  process.stdout.write(
@@ -1837,7 +1874,7 @@ function createProgram() {
1837
1874
  process.stdout.write(renderJson(rows));
1838
1875
  }).catch(fail)
1839
1876
  );
1840
- program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").option("--var <key=value>", "dynamic variable", collect, []).action(
1877
+ program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").option("--implementation-repo <name>").option("--var <key=value>", "dynamic variable", collect, []).action(
1841
1878
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1842
1879
  const result = trace(
1843
1880
  db,
@@ -1852,7 +1889,8 @@ function createProgram() {
1852
1889
  includeAsync: true,
1853
1890
  includeDb: true,
1854
1891
  includeExternal: true,
1855
- vars: parseVars(opts.var)
1892
+ vars: parseVars(opts.var),
1893
+ implementationRepo: opts.implementationRepo
1856
1894
  }
1857
1895
  );
1858
1896
  process.stdout.write(
@@ -1879,64 +1917,7 @@ function createProgram() {
1879
1917
  );
1880
1918
  program.command("doctor").option("--workspace <path>").option("--strict").action(
1881
1919
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1882
- const diagnostics = db.prepare(
1883
- "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id"
1884
- ).all();
1885
- const health = db.prepare(
1886
- `SELECT 'info' severity,'entity_only_service' code,'CDS service has no action/function/event operations; this can be valid for entity-only services' message,s.source_file sourceFile,s.source_line sourceLine
1887
- FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
1888
- UNION ALL
1889
- SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
1890
- FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND s.is_extend=1 AND ?
1891
- UNION ALL
1892
- SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
1893
- FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
1894
- WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
1895
- UNION ALL
1896
- SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
1897
- WHERE NOT EXISTS (SELECT 1 FROM search_index)
1898
- UNION ALL
1899
- SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
1900
- WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)
1901
- UNION ALL
1902
- SELECT 'warning','legacy_schema_weaker_foreign_keys','Legacy table lacks fresh-schema foreign-key metadata; rebuild the database or re-run init/index in a new database',NULL,NULL
1903
- WHERE (SELECT COUNT(*) FROM pragma_foreign_key_list('graph_edges'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('index_runs'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('diagnostics'))=0
1904
- UNION ALL
1905
- SELECT 'warning','implementation_candidates_rejected','Implementation candidates were rejected for ' || s.service_path || o.operation_path,o.source_file,o.source_line
1906
- FROM graph_edges e
1907
- JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
1908
- JOIN cds_services s ON s.id=o.service_id
1909
- WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='unresolved' AND (? OR EXISTS (SELECT 1 FROM graph_edges remote WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' AND remote.to_id=e.from_id))
1910
- UNION ALL
1911
- SELECT 'warning','remote_target_without_implementation','Remote target operation has no implementation edge: ' || s.service_path || o.operation_path,o.source_file,o.source_line
1912
- FROM graph_edges remote
1913
- JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER)
1914
- JOIN cds_services s ON s.id=o.service_id
1915
- WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' AND NOT EXISTS (SELECT 1 FROM graph_edges impl WHERE impl.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND impl.from_kind='operation' AND impl.from_id=remote.to_id) AND ?
1916
- UNION ALL
1917
- SELECT CASE WHEN ? THEN 'warning' ELSE 'error' END,'local_service_calls_all_unresolved','All local service calls are unresolved; verify local service alias parsing and linking',NULL,NULL
1918
- WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE call_type='local_service_call') AND NOT EXISTS (SELECT 1 FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call' AND e.status='resolved')
1919
- UNION ALL
1920
- SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
1921
- FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
1922
- UNION ALL
1923
- SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
1924
- FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
1925
- UNION ALL
1926
- SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
1927
- WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
1928
- UNION ALL
1929
- SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
1930
- WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
1931
- UNION ALL
1932
- SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
1933
- FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
1934
- ).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));
1935
- const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
1936
- const parserQualityHealth = parserQualityDiagnostics(db, Boolean(opts.strict));
1937
- const schemaDriftHealth = schemaDriftDiagnostics(db, Boolean(opts.strict));
1938
- const analyzerVersionHealth = analyzerVersionDiagnostics(db, Boolean(opts.strict));
1939
- const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...schemaDriftHealth, ...analyzerVersionHealth, ...parserQualityHealth];
1920
+ const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict));
1940
1921
  process.stdout.write(
1941
1922
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
1942
1923
  `