@saptools/service-flow 0.1.43 → 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-KHQK7CFH.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.43",
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: [] };
@@ -1254,22 +1263,387 @@ async function indexWorkspace(db, workspaceId, options) {
1254
1263
  let fileCount = 0;
1255
1264
  let diagnosticCount = 0;
1256
1265
  let skippedCount = 0;
1266
+ const preparedRows = [];
1267
+ let activeRepoId;
1257
1268
  try {
1258
1269
  for (const repo of repos) {
1259
- const result = await indexRepository(db, repo, options.force);
1270
+ activeRepoId = repo.id;
1271
+ const result = await prepareRepositoryIndex(repo, options.force);
1272
+ preparedRows.push(result);
1260
1273
  fileCount += result.fileCount;
1261
1274
  diagnosticCount += result.diagnosticCount;
1262
1275
  skippedCount += result.skipped ? 1 : 0;
1263
1276
  }
1264
- materializeCdsExtensionOperations(db, workspaceId);
1265
- 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
+ });
1266
1286
  return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
1267
1287
  } catch (error) {
1288
+ if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
1268
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);
1269
1290
  throw error;
1270
1291
  }
1271
1292
  }
1272
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
+
1273
1647
  // src/trace/selectors.ts
1274
1648
  function parseVars(values) {
1275
1649
  const out = {};
@@ -1383,374 +1757,6 @@ async function withReadOnlyWorkspace(workspace, fn) {
1383
1757
  db.close();
1384
1758
  }
1385
1759
  }
1386
- function schemaDriftDiagnostics(db, strict) {
1387
- if (!strict) return [];
1388
- const symbolColumns = db.prepare("PRAGMA table_info(symbols)").all();
1389
- 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);
1390
- 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();
1391
- const diagnostics = [];
1392
- 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" });
1393
- 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" });
1394
- 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." });
1395
- return diagnostics;
1396
- }
1397
- function linkUpgradeWarnings(db) {
1398
- 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)));
1399
- }
1400
- function analyzerVersionDiagnostics(db, strict) {
1401
- if (!strict) return [];
1402
- 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);
1403
- if (rows.length === 0) return [];
1404
- 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" }];
1405
- }
1406
- function remoteEntityOperationCollisionQuality(db) {
1407
- 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
1408
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1409
- WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
1410
- ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1411
- const examples = [];
1412
- for (const row of rows) {
1413
- const normalized = normalizeODataOperationInvocationPath(String(row.rawPath ?? ""));
1414
- const rawPath = String(row.rawPath ?? "");
1415
- const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
1416
- const name = candidatePath.replace(/^\//, "");
1417
- const simple = name.split(".").at(-1) ?? name;
1418
- const candidates = db.prepare("SELECT COUNT(*) count FROM cds_operations WHERE operation_path IN (?,?) OR operation_name IN (?,?)").get(candidatePath, `/${simple}`, name, simple);
1419
- const candidateCount = Number(candidates.count ?? 0);
1420
- const operationLike = Boolean(normalized?.wasInvocation) || candidateCount > 0;
1421
- if (!operationLike) continue;
1422
- let classifierReason;
1423
- try {
1424
- const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1425
- classifierReason = evidence.odataPathIntent?.reason;
1426
- } catch {
1427
- classifierReason = void 0;
1428
- }
1429
- 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 });
1430
- }
1431
- 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) };
1432
- }
1433
- function remoteEntityDynamicOperationFalsePositiveQuality(db) {
1434
- 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
1435
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1436
- WHERE c.call_type LIKE 'remote_entity_%' AND e.status IN ('dynamic','unresolved') AND e.to_kind='operation_candidate'
1437
- ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1438
- const examples = [];
1439
- for (const row of rows) {
1440
- const rawPath = String(row.rawPath ?? "");
1441
- const method = String(row.method ?? "GET");
1442
- const intent = classifyODataPathIntent(rawPath, method);
1443
- const entityIntent = ["entity_key_read", "entity_navigation_query", "entity_media"].includes(intent.kind) || intent.kind === "entity_mutation" && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
1444
- if (!entityIntent) continue;
1445
- let candidateCount;
1446
- try {
1447
- const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1448
- candidateCount = Number(evidence.indexedOperationCandidateCount ?? evidence.candidateCount ?? 0);
1449
- } catch {
1450
- candidateCount = 0;
1451
- }
1452
- const reason = String(row.unresolvedReason ?? "");
1453
- const keyEvidence = intent.keyPredicatePlaceholderKeys.length > 0 || reason.includes("runtime variable") || reason.includes("placeholder");
1454
- if (candidateCount > 0 || !keyEvidence) continue;
1455
- 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." });
1456
- }
1457
- 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) };
1458
- }
1459
- function localServiceDiagnostics(db, strict) {
1460
- 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();
1461
- const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
1462
- const withoutOwnership = rows.filter((row) => row.reason === "local_service_candidate_without_caller_ownership" || String(row.evidenceJson ?? "").includes("local_service_candidate_without_caller_ownership")).length;
1463
- const unresolved = rows.filter((row) => row.status === "unresolved").length;
1464
- const outsideScope = rows.filter((row) => {
1465
- if (row.status !== "unresolved") return false;
1466
- try {
1467
- const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1468
- return Number(evidence.candidateCount ?? 0) > 0;
1469
- } catch {
1470
- return false;
1471
- }
1472
- }).length;
1473
- const out = [];
1474
- 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}` });
1475
- 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}` });
1476
- if (strict && unresolved > 0) out.push({ severity: "warning", code: "local_service_calls_unresolved", message: `Unresolved local service calls: ${unresolved}` });
1477
- 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}` });
1478
- return out;
1479
- }
1480
- function parserQualityDiagnostics(db, strict) {
1481
- if (!strict) return [];
1482
- const symbolUnresolvedThreshold = 0.05;
1483
- const dbUnknownThreshold = 0.25;
1484
- const outboundUnownedThreshold = 0.01;
1485
- 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();
1486
- 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();
1487
- 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();
1488
- 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();
1489
- 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();
1490
- 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();
1491
- 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();
1492
- 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();
1493
- 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();
1494
- 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();
1495
- const dynamicTerminal = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get();
1496
- 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();
1497
- const ownerlessByCategory = db.prepare(`SELECT CASE
1498
- WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
1499
- WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
1500
- WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
1501
- 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'
1502
- WHEN source_line <= 1 THEN 'unsupported_function_shape'
1503
- WHEN source_line > 1 THEN 'unsupported_callback_shape'
1504
- ELSE 'unknown' END category, COUNT(*) count
1505
- FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY category ORDER BY count DESC, category`).all();
1506
- const ownerlessExamples = db.prepare(`SELECT CASE
1507
- WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
1508
- WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
1509
- WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
1510
- 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'
1511
- WHEN source_line <= 1 THEN 'unsupported_function_shape'
1512
- WHEN source_line > 1 THEN 'unsupported_callback_shape'
1513
- ELSE 'unknown' END category, call_type callType, source_file sourceFile, source_line sourceLine, unresolved_reason unresolvedReason
1514
- FROM outbound_calls WHERE source_symbol_id IS NULL ORDER BY category, source_file, source_line LIMIT 10`).all();
1515
- const symbolTotal = Number(symbol.total ?? 0);
1516
- const symbolUnresolved = Number(symbol.unresolved ?? 0);
1517
- const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
1518
- const queryTotal = Number(dbq.total ?? 0);
1519
- const queryUnknown = Number(dbq.unknown ?? 0);
1520
- const queryUnknownRatio = queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4));
1521
- const outboundTotal = Number(outbound.total ?? 0);
1522
- const outboundWithoutOwnership = Number(outbound.withoutOwnership ?? 0);
1523
- const outboundWithoutOwnershipRatio = outboundTotal === 0 ? 0 : Number((outboundWithoutOwnership / outboundTotal).toFixed(4));
1524
- const remoteQuery = remoteQueryTargetQuality(db);
1525
- const invocation = odataInvocationResolutionQuality(db);
1526
- const remoteAction = remoteActionTargetQuality(db);
1527
- const entityOperationCollision = remoteEntityOperationCollisionQuality(db);
1528
- const entityDynamicFalsePositive = remoteEntityDynamicOperationFalsePositiveQuality(db);
1529
- const externalHttp = externalHttpTargetQuality(db);
1530
- const aliasQuality = identityAliasBindingQuality(db);
1531
- const noBindingQuality = remoteActionNoBindingQuality(db);
1532
- const contextualQuality = contextualImplementationQuality(db);
1533
- const classInstanceQuality = classInstanceNoiseQuality(db);
1534
- const bindingPropagationQuality = contextualBindingPropagationQuality(db);
1535
- const wrapperQuality = wrapperPathPropagationQuality(db);
1536
- const nestedThisQuality = nestedThisReceiverQuality(db);
1537
- return [
1538
- aliasQuality,
1539
- noBindingQuality,
1540
- contextualQuality,
1541
- classInstanceQuality,
1542
- bindingPropagationQuality,
1543
- wrapperQuality,
1544
- nestedThisQuality,
1545
- remoteQuery,
1546
- entityOperationCollision,
1547
- entityDynamicFalsePositive,
1548
- invocation,
1549
- remoteAction,
1550
- externalHttp,
1551
- { 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) },
1552
- { 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 },
1553
- { 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 },
1554
- { 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) },
1555
- { 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) },
1556
- { 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 },
1557
- { 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 },
1558
- { 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 }
1559
- ];
1560
- }
1561
- function identityAliasBindingQuality(db) {
1562
- 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
1563
- FROM outbound_calls c JOIN service_bindings b ON b.repo_id=c.repo_id AND b.source_file=c.source_file
1564
- WHERE c.call_type='remote_action' AND c.service_binding_id IS NULL AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1565
- AND c.evidence_json LIKE '%' || '"aliasOf":"' || json_extract(c.evidence_json,'$.receiver') || '"' || '%'
1566
- ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1567
- 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 };
1568
- }
1569
- function remoteActionNoBindingQuality(db) {
1570
- const categoryCase = `CASE
1571
- WHEN c.unresolved_reason='dynamic_operation_path_identifier' THEN 'dynamic_path_identifier'
1572
- 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'
1573
- WHEN json_extract(c.evidence_json,'$.receiver') LIKE '%.%' THEN 'likely_parameter_context_needed'
1574
- WHEN EXISTS (
1575
- SELECT 1 FROM symbol_calls sc
1576
- JOIN symbols caller ON caller.id=sc.caller_symbol_id
1577
- JOIN symbols callee ON callee.id=sc.callee_symbol_id
1578
- WHERE sc.status='resolved'
1579
- AND sc.source_file=c.source_file
1580
- AND caller.id=c.source_symbol_id
1581
- AND json_extract(sc.evidence_json,'$.relation')='class_instance_method'
1582
- AND (callee.evidence_json IS NULL OR json_extract(callee.evidence_json,'$.parameterBindings') IS NULL)
1583
- ) THEN 'likely_instance_method_parameter_metadata_needed'
1584
- 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'
1585
- WHEN e.status='unresolved' AND COALESCE(e.unresolved_reason,'') LIKE '%No indexed target operation%' THEN 'no_indexed_target_operation'
1586
- 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'
1587
- ELSE 'external_or_entity_path_not_action' END`;
1588
- const rows = db.prepare(`SELECT ${categoryCase} category,COALESCE(e.status,'missing_edge') status,COUNT(*) count
1589
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1590
- WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL
1591
- GROUP BY category,status ORDER BY count DESC,category,status`).all();
1592
- 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
1593
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1594
- 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();
1595
- const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1596
- 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 };
1597
- }
1598
- function classInstanceNoiseQuality(db) {
1599
- 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"];
1600
- const placeholders = builtIns.map(() => "?").join(",");
1601
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1602
- SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
1603
- SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
1604
- FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns);
1605
- const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
1606
- FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
1607
- GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all();
1608
- 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 };
1609
- }
1610
- function contextualBindingPropagationQuality(db) {
1611
- const serviceClientCalls = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc
1612
- WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')`).get();
1613
- const missingMetadata = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1614
- WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1615
- AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get();
1616
- const destructuredUnmapped = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1617
- WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind')='object_literal'
1618
- AND json_extract(s.evidence_json,'$.parameterBindings[0].kind')='object_pattern'
1619
- AND json_array_length(json_extract(sc.evidence_json,'$.callArguments[0].properties')) > json_array_length(json_extract(s.evidence_json,'$.parameterBindings[0].properties'))`).get();
1620
- 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,
1621
- CASE
1622
- WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
1623
- WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
1624
- WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
1625
- ELSE 'trace_time_contextual_binding_candidate'
1626
- END contextualStatus
1627
- FROM outbound_calls c
1628
- LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1629
- LEFT JOIN service_bindings b ON b.id=c.service_binding_id
1630
- LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
1631
- WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1632
- AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
1633
- AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1634
- ORDER BY c.source_file,c.source_line LIMIT 8`).all();
1635
- const statusRows = db.prepare(`SELECT contextualStatus,COUNT(*) count FROM (
1636
- SELECT CASE
1637
- WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
1638
- WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
1639
- WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
1640
- ELSE 'trace_time_contextual_binding_candidate'
1641
- END contextualStatus
1642
- FROM outbound_calls c
1643
- LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1644
- LEFT JOIN service_bindings b ON b.id=c.service_binding_id
1645
- LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
1646
- WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
1647
- AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
1648
- AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
1649
- ) GROUP BY contextualStatus ORDER BY count DESC,contextualStatus`).all();
1650
- 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();
1651
- const totalOpportunities = statusRows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1652
- const actionableStatuses = /* @__PURE__ */ new Set(["alias_without_matching_cds_requires", "cds_requires_present_but_persisted_resolution_unresolved", "trace_time_contextual_binding_candidate"]);
1653
- const actionableOpportunityCount = statusRows.reduce((sum, row) => actionableStatuses.has(String(row.contextualStatus)) ? sum + Number(row.count ?? 0) : sum, 0);
1654
- const severity = Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) + actionableOpportunityCount > 0 ? "warning" : "info";
1655
- 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 };
1656
- }
1657
- function nestedThisReceiverQuality(db) {
1658
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1659
- SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
1660
- SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
1661
- FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get();
1662
- 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
1663
- FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
1664
- ORDER BY source_file,source_line LIMIT 8`).all();
1665
- 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 };
1666
- }
1667
- function contextualImplementationQuality(db) {
1668
- const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count
1669
- 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();
1670
- const examples = db.prepare(`SELECT json_extract(evidence_json,'$.servicePath') servicePath,json_extract(evidence_json,'$.operationPath') operationPath,status,unresolved_reason unresolvedReason,
1671
- json_extract(evidence_json,'$.candidates[0].rejectedReasons[0]') topRejectedReason,
1672
- json_extract(evidence_json,'$.candidates[0].acceptedReasons[0]') topAcceptedReason
1673
- FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') ORDER BY status,id LIMIT 6`).all();
1674
- const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1675
- 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 };
1676
- }
1677
- function wrapperPathPropagationQuality(db) {
1678
- 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
1679
- FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier' ORDER BY source_file,source_line LIMIT 5`).all();
1680
- const aggregate = db.prepare("SELECT COUNT(*) count FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier'").get();
1681
- 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 };
1682
- }
1683
- function remoteQueryTargetQuality(db) {
1684
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1685
- SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.status='terminal' THEN 1 ELSE 0 END) terminal,
1686
- SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
1687
- SUM(CASE WHEN e.edge_type='UNRESOLVED_EDGE' OR e.status='unresolved' THEN 1 ELSE 0 END) unresolved
1688
- 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();
1689
- 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
1690
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1691
- 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]*')
1692
- ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1693
- const numericTargets = Number(aggregate.numericTargets ?? 0);
1694
- const unresolved = Number(aggregate.unresolved ?? 0);
1695
- 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 };
1696
- }
1697
- function remoteActionTargetQuality(db) {
1698
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1699
- SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved,
1700
- SUM(CASE WHEN e.status='unresolved' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
1701
- 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
1702
- 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();
1703
- 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
1704
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1705
- 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();
1706
- const numericTargets = Number(aggregate.numericTargets ?? 0);
1707
- 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 };
1708
- }
1709
- function externalHttpTargetQuality(db) {
1710
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1711
- SUM(CASE WHEN e.to_kind='external_destination' THEN 1 ELSE 0 END) destinationTargets,
1712
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.kind')='static_url' THEN 1 ELSE 0 END) staticEndpointTargets,
1713
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.dynamic')=1 THEN 1 ELSE 0 END) dynamicEndpointTargets,
1714
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.kind')='unknown' THEN 1 ELSE 0 END) unknownEndpointTargets,
1715
- SUM(CASE WHEN e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
1716
- 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
1717
- 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();
1718
- 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
1719
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1720
- 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)
1721
- ORDER BY c.source_file,c.source_line LIMIT 5`).all();
1722
- const numericTargets = Number(aggregate.numericTargets ?? 0);
1723
- const invalidEvidence = Number(aggregate.invalidEvidence ?? 0);
1724
- 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 };
1725
- }
1726
- function odataInvocationResolutionQuality(db) {
1727
- const aggregate = db.prepare(`SELECT COUNT(*) total,
1728
- SUM(CASE WHEN e.status='resolved' THEN 1 ELSE 0 END) resolved,
1729
- SUM(CASE WHEN e.status='dynamic' THEN 1 ELSE 0 END) dynamic,
1730
- SUM(CASE WHEN e.status='ambiguous' THEN 1 ELSE 0 END) ambiguous,
1731
- SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved
1732
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1733
- WHERE c.call_type='remote_action' AND c.operation_path_expr LIKE '%(%'`).get();
1734
- 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
1735
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1736
- WHERE c.call_type='remote_action' AND e.status IN ('unresolved','ambiguous') AND c.operation_path_expr LIKE '%(%'
1737
- ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1738
- const examples = [];
1739
- let unresolvedMatchingIndexedOperation = 0;
1740
- let ambiguousNormalizedCalls = 0;
1741
- for (const row of rows) {
1742
- const normalized = normalizeODataOperationInvocationPath(row.operationPathExpr);
1743
- if (!normalized?.wasInvocation) continue;
1744
- const normalizedName = normalized.normalizedOperationPath.replace(/^\//, "");
1745
- const simpleName = normalizedName.split(".").at(-1) ?? normalizedName;
1746
- 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);
1747
- if (candidates.length === 0) continue;
1748
- if (row.status === "ambiguous") ambiguousNormalizedCalls += 1;
1749
- if (row.status === "unresolved") unresolvedMatchingIndexedOperation += 1;
1750
- if (examples.length < 5) examples.push({ sourceFile: row.sourceFile, sourceLine: row.sourceLine, rawOperationPath: row.operationPathExpr, normalizedOperationPath: normalized.normalizedOperationPath, candidateCount: candidates.length, candidates });
1751
- }
1752
- 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 };
1753
- }
1754
1760
  function createProgram() {
1755
1761
  const program = new Command();
1756
1762
  program.name("service-flow").description(
@@ -1782,7 +1788,7 @@ function createProgram() {
1782
1788
  );
1783
1789
  }).catch(fail)
1784
1790
  );
1785
- 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(
1786
1792
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1787
1793
  const result = trace(
1788
1794
  db,
@@ -1798,7 +1804,8 @@ function createProgram() {
1798
1804
  vars: parseVars(opts.var),
1799
1805
  includeExternal: Boolean(opts.includeExternal),
1800
1806
  includeDb: Boolean(opts.includeDb),
1801
- includeAsync: Boolean(opts.includeAsync)
1807
+ includeAsync: Boolean(opts.includeAsync),
1808
+ implementationRepo: opts.implementationRepo
1802
1809
  }
1803
1810
  );
1804
1811
  process.stdout.write(
@@ -1867,7 +1874,7 @@ function createProgram() {
1867
1874
  process.stdout.write(renderJson(rows));
1868
1875
  }).catch(fail)
1869
1876
  );
1870
- 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(
1871
1878
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1872
1879
  const result = trace(
1873
1880
  db,
@@ -1882,7 +1889,8 @@ function createProgram() {
1882
1889
  includeAsync: true,
1883
1890
  includeDb: true,
1884
1891
  includeExternal: true,
1885
- vars: parseVars(opts.var)
1892
+ vars: parseVars(opts.var),
1893
+ implementationRepo: opts.implementationRepo
1886
1894
  }
1887
1895
  );
1888
1896
  process.stdout.write(
@@ -1909,64 +1917,7 @@ function createProgram() {
1909
1917
  );
1910
1918
  program.command("doctor").option("--workspace <path>").option("--strict").action(
1911
1919
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1912
- const diagnostics = db.prepare(
1913
- "SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id"
1914
- ).all();
1915
- const health = db.prepare(
1916
- `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
1917
- FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
1918
- UNION ALL
1919
- SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
1920
- 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 ?
1921
- UNION ALL
1922
- SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
1923
- FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
1924
- WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
1925
- UNION ALL
1926
- SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
1927
- WHERE NOT EXISTS (SELECT 1 FROM search_index)
1928
- UNION ALL
1929
- SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
1930
- WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)
1931
- UNION ALL
1932
- 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
1933
- 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
1934
- UNION ALL
1935
- SELECT 'warning','implementation_candidates_rejected','Implementation candidates were rejected for ' || s.service_path || o.operation_path,o.source_file,o.source_line
1936
- FROM graph_edges e
1937
- JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
1938
- JOIN cds_services s ON s.id=o.service_id
1939
- 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))
1940
- UNION ALL
1941
- 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
1942
- FROM graph_edges remote
1943
- JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER)
1944
- JOIN cds_services s ON s.id=o.service_id
1945
- 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 ?
1946
- UNION ALL
1947
- 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
1948
- 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')
1949
- UNION ALL
1950
- SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
1951
- FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
1952
- UNION ALL
1953
- SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
1954
- FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
1955
- UNION ALL
1956
- SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
1957
- WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
1958
- UNION ALL
1959
- SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
1960
- WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
1961
- UNION ALL
1962
- SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
1963
- FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
1964
- ).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));
1965
- const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
1966
- const parserQualityHealth = parserQualityDiagnostics(db, Boolean(opts.strict));
1967
- const schemaDriftHealth = schemaDriftDiagnostics(db, Boolean(opts.strict));
1968
- const analyzerVersionHealth = analyzerVersionDiagnostics(db, Boolean(opts.strict));
1969
- const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...schemaDriftHealth, ...analyzerVersionHealth, ...parserQualityHealth];
1920
+ const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict));
1970
1921
  process.stdout.write(
1971
1922
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
1972
1923
  `