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