@saptools/service-flow 0.1.43 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -21,13 +21,13 @@ import { parsePackageJson } from './parsers/package-json-parser.js';
21
21
  import { classifyRepository } from './discovery/classify-repository.js';
22
22
  import { indexWorkspace } from './indexer/workspace-indexer.js';
23
23
  import { linkWorkspace } from './linker/cross-repo-linker.js';
24
- import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from './linker/odata-path-normalizer.js';
24
+ import { doctorDiagnostics, linkUpgradeWarnings } from './cli/doctor.js';
25
25
  import { trace } from './trace/trace-engine.js';
26
26
  import { parseVars } from './trace/selectors.js';
27
27
  import { renderTraceTable } from './output/table-output.js';
28
28
  import { renderTraceJson, renderJson } from './output/json-output.js';
29
29
  import { renderMermaid } from './output/mermaid-output.js';
30
- import { ANALYZER_VERSION, VERSION } from './version.js';
30
+ import { VERSION } from './version.js';
31
31
  async function init(
32
32
  workspace: string,
33
33
  options: { db?: string; ignore?: string[] },
@@ -90,394 +90,6 @@ async function withReadOnlyWorkspace<T>(
90
90
  db.close();
91
91
  }
92
92
  }
93
- function schemaDriftDiagnostics(db: ReturnType<typeof openDatabase>, strict: boolean): Array<Record<string, unknown>> {
94
- if (!strict) return [];
95
- const symbolColumns = db.prepare("PRAGMA table_info(symbols)").all() as Array<{ name?: string }>;
96
- 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);
97
- 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() as Array<Record<string, unknown>>;
98
- const diagnostics: Array<Record<string, unknown>> = [];
99
- 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' });
100
- 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' });
101
- 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.' });
102
- return diagnostics;
103
- }
104
- function linkUpgradeWarnings(db: ReturnType<typeof openDatabase>): Array<Record<string, unknown>> {
105
- 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)));
106
- }
107
-
108
- function analyzerVersionDiagnostics(db: ReturnType<typeof openDatabase>, strict: boolean): Array<Record<string, unknown>> {
109
- if (!strict) return [];
110
- 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) as Array<Record<string, unknown>>;
111
- if (rows.length === 0) return [];
112
- 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' }];
113
- }
114
-
115
- function remoteEntityOperationCollisionQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
116
- 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
117
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
118
- WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
119
- ORDER BY c.source_file,c.source_line LIMIT 100`).all() as Array<Record<string, unknown>>;
120
- const examples: Array<Record<string, unknown>> = [];
121
- for (const row of rows) {
122
- const normalized = normalizeODataOperationInvocationPath(String(row.rawPath ?? ''));
123
- const rawPath = String(row.rawPath ?? '');
124
- const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
125
- const name = candidatePath.replace(/^\//, '');
126
- const simple = name.split('.').at(-1) ?? name;
127
- const candidates = db.prepare('SELECT COUNT(*) count FROM cds_operations WHERE operation_path IN (?,?) OR operation_name IN (?,?)').get(candidatePath, `/${simple}`, name, simple) as { count?: number };
128
- const candidateCount = Number(candidates.count ?? 0);
129
- const operationLike = Boolean(normalized?.wasInvocation) || candidateCount > 0;
130
- if (!operationLike) continue;
131
- let classifierReason: unknown;
132
- try {
133
- const evidence = JSON.parse(String(row.evidenceJson ?? '{}')) as { odataPathIntent?: { reason?: unknown } };
134
- classifierReason = evidence.odataPathIntent?.reason;
135
- } catch {
136
- classifierReason = undefined;
137
- }
138
- 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 });
139
- }
140
- 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) };
141
- }
142
-
143
-
144
- function remoteEntityDynamicOperationFalsePositiveQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
145
- 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
146
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
147
- WHERE c.call_type LIKE 'remote_entity_%' AND e.status IN ('dynamic','unresolved') AND e.to_kind='operation_candidate'
148
- ORDER BY c.source_file,c.source_line LIMIT 100`).all() as Array<Record<string, unknown>>;
149
- const examples: Array<Record<string, unknown>> = [];
150
- for (const row of rows) {
151
- const rawPath = String(row.rawPath ?? '');
152
- const method = String(row.method ?? 'GET');
153
- const intent = classifyODataPathIntent(rawPath, method);
154
- const entityIntent = ['entity_key_read', 'entity_navigation_query', 'entity_media'].includes(intent.kind) || (intent.kind === 'entity_mutation' && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix));
155
- if (!entityIntent) continue;
156
- let candidateCount: number;
157
- try {
158
- const evidence = JSON.parse(String(row.evidenceJson ?? '{}')) as { indexedOperationCandidateCount?: unknown; candidateCount?: unknown };
159
- candidateCount = Number(evidence.indexedOperationCandidateCount ?? evidence.candidateCount ?? 0);
160
- } catch {
161
- candidateCount = 0;
162
- }
163
- const reason = String(row.unresolvedReason ?? '');
164
- const keyEvidence = intent.keyPredicatePlaceholderKeys.length > 0 || reason.includes('runtime variable') || reason.includes('placeholder');
165
- if (candidateCount > 0 || !keyEvidence) continue;
166
- 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.' });
167
- }
168
- 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) };
169
- }
170
-
171
- function localServiceDiagnostics(db: ReturnType<typeof openDatabase>, strict: boolean): Array<Record<string, unknown>> {
172
- 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() as Array<{ status?: string; reason?: string | null; evidenceJson?: string }>;
173
- const implementationContext = rows.filter((row) => row.status === 'resolved' && String(row.evidenceJson ?? '').includes('implementation_context_caller_ownership')).length;
174
- const withoutOwnership = rows.filter((row) => row.reason === 'local_service_candidate_without_caller_ownership' || String(row.evidenceJson ?? '').includes('local_service_candidate_without_caller_ownership')).length;
175
- const unresolved = rows.filter((row) => row.status === 'unresolved').length;
176
- const outsideScope = rows.filter((row) => {
177
- if (row.status !== 'unresolved') return false;
178
- try {
179
- const evidence = JSON.parse(String(row.evidenceJson ?? '{}')) as { candidateCount?: unknown };
180
- return Number(evidence.candidateCount ?? 0) > 0;
181
- } catch {
182
- return false;
183
- }
184
- }).length;
185
- const out: Array<Record<string, unknown>> = [];
186
- 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}` });
187
- 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}` });
188
- if (strict && unresolved > 0) out.push({ severity: 'warning', code: 'local_service_calls_unresolved', message: `Unresolved local service calls: ${unresolved}` });
189
- 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}` });
190
- return out;
191
- }
192
-
193
- function parserQualityDiagnostics(db: ReturnType<typeof openDatabase>, strict: boolean): Array<Record<string, unknown>> {
194
- if (!strict) return [];
195
- const symbolUnresolvedThreshold = 0.05;
196
- const dbUnknownThreshold = 0.25;
197
- const outboundUnownedThreshold = 0.01;
198
- 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() as { total?: number; resolved?: number; unresolved?: number };
199
- 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() as Array<Record<string, unknown>>;
200
- 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() as { total?: number; nonObject?: number };
201
- 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() as { total?: number; known?: number; unknown?: number };
202
- 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() as { total?: number; withoutOwnership?: number };
203
- 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() as { total?: number; missing?: number; invalid?: number; nonObject?: number };
204
- 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() as Array<Record<string, unknown>>;
205
- 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() as { total?: number; nonObject?: number; withOutboundEvidence?: number };
206
- 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() as Array<Record<string, unknown>>;
207
- 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() as { total?: number; eventTotal?: number; questionable?: number };
208
- const dynamicTerminal = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get() as { count?: number };
209
- 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() as Array<Record<string, unknown>>;
210
- const ownerlessByCategory = db.prepare(`SELECT CASE
211
- WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
212
- WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
213
- WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
214
- 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'
215
- WHEN source_line <= 1 THEN 'unsupported_function_shape'
216
- WHEN source_line > 1 THEN 'unsupported_callback_shape'
217
- ELSE 'unknown' END category, COUNT(*) count
218
- FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY category ORDER BY count DESC, category`).all() as Array<Record<string, unknown>>;
219
- const ownerlessExamples = db.prepare(`SELECT CASE
220
- WHEN COALESCE(evidence_json,'') LIKE '%comment_or_non_executable_source%' THEN 'comment_or_non_executable_source'
221
- WHEN call_type='async_subscribe' AND COALESCE(evidence_json,'') LIKE '%cap_service_event_subscription%' THEN 'top_level_event_registration'
222
- WHEN call_type='async_subscribe' THEN 'generic_event_listener_ignored_or_unowned'
223
- 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'
224
- WHEN source_line <= 1 THEN 'unsupported_function_shape'
225
- WHEN source_line > 1 THEN 'unsupported_callback_shape'
226
- ELSE 'unknown' END category, call_type callType, source_file sourceFile, source_line sourceLine, unresolved_reason unresolvedReason
227
- FROM outbound_calls WHERE source_symbol_id IS NULL ORDER BY category, source_file, source_line LIMIT 10`).all() as Array<Record<string, unknown>>;
228
- const symbolTotal = Number(symbol.total ?? 0);
229
- const symbolUnresolved = Number(symbol.unresolved ?? 0);
230
- const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
231
- const queryTotal = Number(dbq.total ?? 0);
232
- const queryUnknown = Number(dbq.unknown ?? 0);
233
- const queryUnknownRatio = queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4));
234
- const outboundTotal = Number(outbound.total ?? 0);
235
- const outboundWithoutOwnership = Number(outbound.withoutOwnership ?? 0);
236
- const outboundWithoutOwnershipRatio = outboundTotal === 0 ? 0 : Number((outboundWithoutOwnership / outboundTotal).toFixed(4));
237
- const remoteQuery = remoteQueryTargetQuality(db);
238
- const invocation = odataInvocationResolutionQuality(db);
239
- const remoteAction = remoteActionTargetQuality(db);
240
- const entityOperationCollision = remoteEntityOperationCollisionQuality(db);
241
- const entityDynamicFalsePositive = remoteEntityDynamicOperationFalsePositiveQuality(db);
242
- const externalHttp = externalHttpTargetQuality(db);
243
- const aliasQuality = identityAliasBindingQuality(db);
244
- const noBindingQuality = remoteActionNoBindingQuality(db);
245
- const contextualQuality = contextualImplementationQuality(db);
246
- const classInstanceQuality = classInstanceNoiseQuality(db);
247
- const bindingPropagationQuality = contextualBindingPropagationQuality(db);
248
- const wrapperQuality = wrapperPathPropagationQuality(db);
249
- const nestedThisQuality = nestedThisReceiverQuality(db);
250
- return [
251
- aliasQuality,
252
- noBindingQuality,
253
- contextualQuality,
254
- classInstanceQuality,
255
- bindingPropagationQuality,
256
- wrapperQuality,
257
- nestedThisQuality,
258
- remoteQuery,
259
- entityOperationCollision,
260
- entityDynamicFalsePositive,
261
- invocation,
262
- remoteAction,
263
- externalHttp,
264
- { 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) },
265
- { 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 },
266
- { 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 },
267
- { 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) },
268
- { 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) },
269
- { 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 },
270
- { 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 },
271
- { 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 },
272
- ];
273
- }
274
-
275
-
276
- function identityAliasBindingQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
277
- 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
278
- FROM outbound_calls c JOIN service_bindings b ON b.repo_id=c.repo_id AND b.source_file=c.source_file
279
- WHERE c.call_type='remote_action' AND c.service_binding_id IS NULL AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
280
- AND c.evidence_json LIKE '%' || '"aliasOf":"' || json_extract(c.evidence_json,'$.receiver') || '"' || '%'
281
- ORDER BY c.source_file,c.source_line LIMIT 5`).all() as Array<Record<string, unknown>>;
282
- 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 };
283
- }
284
-
285
- function remoteActionNoBindingQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
286
- const categoryCase = `CASE
287
- WHEN c.unresolved_reason='dynamic_operation_path_identifier' THEN 'dynamic_path_identifier'
288
- 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'
289
- WHEN json_extract(c.evidence_json,'$.receiver') LIKE '%.%' THEN 'likely_parameter_context_needed'
290
- WHEN EXISTS (
291
- SELECT 1 FROM symbol_calls sc
292
- JOIN symbols caller ON caller.id=sc.caller_symbol_id
293
- JOIN symbols callee ON callee.id=sc.callee_symbol_id
294
- WHERE sc.status='resolved'
295
- AND sc.source_file=c.source_file
296
- AND caller.id=c.source_symbol_id
297
- AND json_extract(sc.evidence_json,'$.relation')='class_instance_method'
298
- AND (callee.evidence_json IS NULL OR json_extract(callee.evidence_json,'$.parameterBindings') IS NULL)
299
- ) THEN 'likely_instance_method_parameter_metadata_needed'
300
- 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'
301
- WHEN e.status='unresolved' AND COALESCE(e.unresolved_reason,'') LIKE '%No indexed target operation%' THEN 'no_indexed_target_operation'
302
- 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'
303
- ELSE 'external_or_entity_path_not_action' END`;
304
- const rows = db.prepare(`SELECT ${categoryCase} category,COALESCE(e.status,'missing_edge') status,COUNT(*) count
305
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
306
- WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL
307
- GROUP BY category,status ORDER BY count DESC,category,status`).all() as Array<Record<string, unknown>>;
308
- 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
309
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
310
- 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() as Array<Record<string, unknown>>;
311
- const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
312
- 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 };
313
- }
314
-
315
- function classInstanceNoiseQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
316
- 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'];
317
- const placeholders = builtIns.map(() => '?').join(',');
318
- const aggregate = db.prepare(`SELECT COUNT(*) total,
319
- SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
320
- SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
321
- FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns) as { total?: number; unresolved?: number; unresolvedBuiltIn?: number };
322
- const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
323
- FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
324
- GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all() as Array<Record<string, unknown>>;
325
- 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 };
326
- }
327
-
328
- function contextualBindingPropagationQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
329
- const serviceClientCalls = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc
330
- WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')`).get() as { count?: number };
331
- const missingMetadata = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
332
- WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
333
- AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get() as { count?: number };
334
- const destructuredUnmapped = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
335
- WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind')='object_literal'
336
- AND json_extract(s.evidence_json,'$.parameterBindings[0].kind')='object_pattern'
337
- AND json_array_length(json_extract(sc.evidence_json,'$.callArguments[0].properties')) > json_array_length(json_extract(s.evidence_json,'$.parameterBindings[0].properties'))`).get() as { count?: number };
338
- 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,
339
- CASE
340
- WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
341
- WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
342
- WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
343
- ELSE 'trace_time_contextual_binding_candidate'
344
- END contextualStatus
345
- FROM outbound_calls c
346
- LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
347
- LEFT JOIN service_bindings b ON b.id=c.service_binding_id
348
- LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
349
- WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
350
- AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
351
- AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
352
- ORDER BY c.source_file,c.source_line LIMIT 8`).all() as Array<Record<string, unknown>>;
353
- const statusRows = db.prepare(`SELECT contextualStatus,COUNT(*) count FROM (
354
- SELECT CASE
355
- WHEN (b.alias_expr LIKE '%$%' OR b.service_path_expr LIKE '%$%' OR b.destination_expr LIKE '%$%') THEN 'runtime_variables_required'
356
- WHEN b.alias IS NOT NULL AND req.id IS NULL AND b.service_path_expr IS NULL THEN 'alias_without_matching_cds_requires'
357
- WHEN req.id IS NOT NULL AND COALESCE(e.status,'missing_edge')!='resolved' THEN 'cds_requires_present_but_persisted_resolution_unresolved'
358
- ELSE 'trace_time_contextual_binding_candidate'
359
- END contextualStatus
360
- FROM outbound_calls c
361
- LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
362
- LEFT JOIN service_bindings b ON b.id=c.service_binding_id
363
- LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
364
- WHERE c.call_type='remote_action' AND json_extract(c.evidence_json,'$.receiver') IS NOT NULL
365
- AND (c.service_binding_id IS NULL OR e.status IS NULL OR e.status!='resolved')
366
- AND EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND sc.source_file=c.source_file)
367
- ) GROUP BY contextualStatus ORDER BY count DESC,contextualStatus`).all() as Array<Record<string, unknown>>;
368
- 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() as { count?: number };
369
- const totalOpportunities = statusRows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
370
- const actionableStatuses = new Set(['alias_without_matching_cds_requires', 'cds_requires_present_but_persisted_resolution_unresolved', 'trace_time_contextual_binding_candidate']);
371
- const actionableOpportunityCount = statusRows.reduce((sum, row) => actionableStatuses.has(String(row.contextualStatus)) ? sum + Number(row.count ?? 0) : sum, 0);
372
- const severity = Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) + actionableOpportunityCount > 0 ? 'warning' : 'info';
373
- 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 };
374
- }
375
-
376
- function nestedThisReceiverQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
377
- const aggregate = db.prepare(`SELECT COUNT(*) total,
378
- SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
379
- SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
380
- FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get() as { total?: number; resolvedToCurrentClass?: number; withExplicitHelperInstanceEvidence?: number };
381
- 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
382
- FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
383
- ORDER BY source_file,source_line LIMIT 8`).all() as Array<Record<string, unknown>>;
384
- 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 };
385
- }
386
-
387
- function contextualImplementationQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
388
- const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count
389
- 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() as Array<Record<string, unknown>>;
390
- const examples = db.prepare(`SELECT json_extract(evidence_json,'$.servicePath') servicePath,json_extract(evidence_json,'$.operationPath') operationPath,status,unresolved_reason unresolvedReason,
391
- json_extract(evidence_json,'$.candidates[0].rejectedReasons[0]') topRejectedReason,
392
- json_extract(evidence_json,'$.candidates[0].acceptedReasons[0]') topAcceptedReason
393
- FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') ORDER BY status,id LIMIT 6`).all() as Array<Record<string, unknown>>;
394
- const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
395
- 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 };
396
- }
397
-
398
- function wrapperPathPropagationQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
399
- 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
400
- FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier' ORDER BY source_file,source_line LIMIT 5`).all() as Array<Record<string, unknown>>;
401
- const aggregate = db.prepare("SELECT COUNT(*) count FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier'").get() as { count?: number };
402
- 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 };
403
- }
404
-
405
- function remoteQueryTargetQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
406
- const aggregate = db.prepare(`SELECT COUNT(*) total,
407
- SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.status='terminal' THEN 1 ELSE 0 END) terminal,
408
- SUM(CASE WHEN e.edge_type='HANDLER_RUNS_REMOTE_QUERY' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
409
- SUM(CASE WHEN e.edge_type='UNRESOLVED_EDGE' OR e.status='unresolved' THEN 1 ELSE 0 END) unresolved
410
- 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() as { total?: number; terminal?: number; numericTargets?: number; unresolved?: number };
411
- 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
412
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
413
- 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]*')
414
- ORDER BY c.source_file,c.source_line LIMIT 5`).all() as Array<Record<string, unknown>>;
415
- const numericTargets = Number(aggregate.numericTargets ?? 0);
416
- const unresolved = Number(aggregate.unresolved ?? 0);
417
- 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 };
418
- }
419
-
420
- function remoteActionTargetQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
421
- const aggregate = db.prepare(`SELECT COUNT(*) total,
422
- SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved,
423
- SUM(CASE WHEN e.status='unresolved' AND e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
424
- 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
425
- 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() as { total?: number; unresolved?: number; numericTargets?: number; semanticTargets?: number };
426
- 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
427
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
428
- 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() as Array<Record<string, unknown>>;
429
- const numericTargets = Number(aggregate.numericTargets ?? 0);
430
- 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 };
431
- }
432
-
433
-
434
- function externalHttpTargetQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
435
- const aggregate = db.prepare(`SELECT COUNT(*) total,
436
- SUM(CASE WHEN e.to_kind='external_destination' THEN 1 ELSE 0 END) destinationTargets,
437
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.kind')='static_url' THEN 1 ELSE 0 END) staticEndpointTargets,
438
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.dynamic')=1 THEN 1 ELSE 0 END) dynamicEndpointTargets,
439
- SUM(CASE WHEN e.to_kind='external_endpoint' AND json_extract(e.evidence_json,'$.externalTarget.kind')='unknown' THEN 1 ELSE 0 END) unknownEndpointTargets,
440
- SUM(CASE WHEN e.to_id GLOB '[0-9]*' THEN 1 ELSE 0 END) numericTargets,
441
- 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
442
- 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() as Record<string, unknown>;
443
- 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
444
- FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
445
- 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)
446
- ORDER BY c.source_file,c.source_line LIMIT 5`).all() as Array<Record<string, unknown>>;
447
- const numericTargets = Number(aggregate.numericTargets ?? 0);
448
- const invalidEvidence = Number(aggregate.invalidEvidence ?? 0);
449
- 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 };
450
- }
451
-
452
- function odataInvocationResolutionQuality(db: ReturnType<typeof openDatabase>): Record<string, unknown> {
453
- const aggregate = db.prepare(`SELECT COUNT(*) total,
454
- SUM(CASE WHEN e.status='resolved' THEN 1 ELSE 0 END) resolved,
455
- SUM(CASE WHEN e.status='dynamic' THEN 1 ELSE 0 END) dynamic,
456
- SUM(CASE WHEN e.status='ambiguous' THEN 1 ELSE 0 END) ambiguous,
457
- SUM(CASE WHEN e.status='unresolved' THEN 1 ELSE 0 END) unresolved
458
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
459
- WHERE c.call_type='remote_action' AND c.operation_path_expr LIKE '%(%'`) .get() as { total?: number; resolved?: number; dynamic?: number; ambiguous?: number; unresolved?: number };
460
- 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
461
- FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
462
- WHERE c.call_type='remote_action' AND e.status IN ('unresolved','ambiguous') AND c.operation_path_expr LIKE '%(%'
463
- ORDER BY c.source_file,c.source_line LIMIT 100`).all() as Array<{ id?: number; operationPathExpr?: string; sourceFile?: string; sourceLine?: number; status?: string; unresolvedReason?: string }>;
464
- const examples: Array<Record<string, unknown>> = [];
465
- let unresolvedMatchingIndexedOperation = 0;
466
- let ambiguousNormalizedCalls = 0;
467
- for (const row of rows) {
468
- const normalized = normalizeODataOperationInvocationPath(row.operationPathExpr);
469
- if (!normalized?.wasInvocation) continue;
470
- const normalizedName = normalized.normalizedOperationPath.replace(/^\//, '');
471
- const simpleName = normalizedName.split('.').at(-1) ?? normalizedName;
472
- 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) as Array<Record<string, unknown>>;
473
- if (candidates.length === 0) continue;
474
- if (row.status === 'ambiguous') ambiguousNormalizedCalls += 1;
475
- if (row.status === 'unresolved') unresolvedMatchingIndexedOperation += 1;
476
- if (examples.length < 5) examples.push({ sourceFile: row.sourceFile, sourceLine: row.sourceLine, rawOperationPath: row.operationPathExpr, normalizedOperationPath: normalized.normalizedOperationPath, candidateCount: candidates.length, candidates });
477
- }
478
- 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 };
479
- }
480
-
481
93
  export function createProgram(): Command {
482
94
  const program = new Command();
483
95
  program
@@ -539,6 +151,7 @@ export function createProgram(): Command {
539
151
  .option('--include-external')
540
152
  .option('--include-db')
541
153
  .option('--include-async')
154
+ .option('--implementation-repo <name>')
542
155
  .option('--var <key=value>', 'dynamic variable', collect, [])
543
156
  .action(
544
157
  (opts: {
@@ -553,6 +166,7 @@ export function createProgram(): Command {
553
166
  includeExternal?: boolean;
554
167
  includeDb?: boolean;
555
168
  includeAsync?: boolean;
169
+ implementationRepo?: string;
556
170
  var: string[];
557
171
  }) =>
558
172
  void withReadOnlyWorkspace(opts.workspace, (db) => {
@@ -571,6 +185,7 @@ export function createProgram(): Command {
571
185
  includeExternal: Boolean(opts.includeExternal),
572
186
  includeDb: Boolean(opts.includeDb),
573
187
  includeAsync: Boolean(opts.includeAsync),
188
+ implementationRepo: opts.implementationRepo,
574
189
  },
575
190
  );
576
191
  process.stdout.write(
@@ -677,6 +292,7 @@ export function createProgram(): Command {
677
292
  .option('--service <path>')
678
293
  .option('--path <operationPath>')
679
294
  .option('--format <format>', 'mermaid|json', 'mermaid')
295
+ .option('--implementation-repo <name>')
680
296
  .option('--var <key=value>', 'dynamic variable', collect, [])
681
297
  .action(
682
298
  (opts: {
@@ -686,6 +302,7 @@ export function createProgram(): Command {
686
302
  service?: string;
687
303
  path?: string;
688
304
  format: string;
305
+ implementationRepo?: string;
689
306
  var: string[];
690
307
  }) =>
691
308
  void withReadOnlyWorkspace(opts.workspace, (db) => {
@@ -703,6 +320,7 @@ export function createProgram(): Command {
703
320
  includeDb: true,
704
321
  includeExternal: true,
705
322
  vars: parseVars(opts.var),
323
+ implementationRepo: opts.implementationRepo,
706
324
  },
707
325
  );
708
326
  process.stdout.write(
@@ -747,68 +365,7 @@ export function createProgram(): Command {
747
365
  .action(
748
366
  (opts: { workspace?: string; strict?: boolean }) =>
749
367
  void withReadOnlyWorkspace(opts.workspace, (db) => {
750
- const diagnostics = db
751
- .prepare(
752
- 'SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id',
753
- )
754
- .all() as Array<Record<string, unknown>>;
755
- const health = db
756
- .prepare(
757
- `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
758
- FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
759
- UNION ALL
760
- SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
761
- 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 ?
762
- UNION ALL
763
- SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
764
- FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
765
- WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
766
- UNION ALL
767
- SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
768
- WHERE NOT EXISTS (SELECT 1 FROM search_index)
769
- UNION ALL
770
- SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
771
- WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)
772
- UNION ALL
773
- 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
774
- 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
775
- UNION ALL
776
- SELECT 'warning','implementation_candidates_rejected','Implementation candidates were rejected for ' || s.service_path || o.operation_path,o.source_file,o.source_line
777
- FROM graph_edges e
778
- JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
779
- JOIN cds_services s ON s.id=o.service_id
780
- 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))
781
- UNION ALL
782
- 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
783
- FROM graph_edges remote
784
- JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER)
785
- JOIN cds_services s ON s.id=o.service_id
786
- 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 ?
787
- UNION ALL
788
- 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
789
- 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')
790
- UNION ALL
791
- SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
792
- FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
793
- UNION ALL
794
- SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
795
- FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
796
- UNION ALL
797
- SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
798
- WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
799
- UNION ALL
800
- SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
801
- WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
802
- UNION ALL
803
- SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
804
- FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`,
805
- )
806
- .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)) as Array<Record<string, unknown>>;
807
- const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
808
- const parserQualityHealth = parserQualityDiagnostics(db, Boolean(opts.strict));
809
- const schemaDriftHealth = schemaDriftDiagnostics(db, Boolean(opts.strict));
810
- const analyzerVersionHealth = analyzerVersionDiagnostics(db, Boolean(opts.strict));
811
- const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...schemaDriftHealth, ...analyzerVersionHealth, ...parserQualityHealth];
368
+ const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict));
812
369
  process.stdout.write(
813
370
  allDiagnostics.length
814
371
  ? renderJson(allDiagnostics)