@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/index.d.ts
CHANGED
|
@@ -205,6 +205,7 @@ declare function trace(db: Db, start: TraceStart, options: {
|
|
|
205
205
|
includeExternal?: boolean;
|
|
206
206
|
includeDb?: boolean;
|
|
207
207
|
includeAsync?: boolean;
|
|
208
|
+
implementationRepo?: string;
|
|
208
209
|
}): TraceResult;
|
|
209
210
|
|
|
210
211
|
declare function redactText(text: string): string;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
3
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
4
|
+
|
|
5
|
+
type Diagnostic = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export function linkUpgradeWarnings(db: Db): Diagnostic[] {
|
|
8
|
+
return [...schemaDriftDiagnostics(db, true), ...analyzerVersionDiagnostics(db, true)]
|
|
9
|
+
.filter((item) => ['schema_legacy_columns_present', 'external_target_columns_missing_data', 'reindex_required_after_upgrade', 'reindex_required_after_analyzer_upgrade'].includes(String(item.code)));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function doctorDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
13
|
+
const diagnostics = db.prepare('SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id').all() as Diagnostic[];
|
|
14
|
+
return [
|
|
15
|
+
...diagnostics,
|
|
16
|
+
...healthDiagnostics(db, strict),
|
|
17
|
+
...localServiceDiagnostics(db, strict),
|
|
18
|
+
...schemaDriftDiagnostics(db, strict),
|
|
19
|
+
...analyzerVersionDiagnostics(db, strict),
|
|
20
|
+
...parserQualityDiagnostics(db, strict),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function healthDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
25
|
+
return db.prepare(
|
|
26
|
+
`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
|
|
27
|
+
FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
|
|
28
|
+
UNION ALL
|
|
29
|
+
SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
|
|
30
|
+
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 ?
|
|
31
|
+
UNION ALL
|
|
32
|
+
SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
|
|
33
|
+
FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
|
|
34
|
+
WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
|
|
35
|
+
UNION ALL
|
|
36
|
+
SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
|
|
37
|
+
WHERE NOT EXISTS (SELECT 1 FROM search_index)
|
|
38
|
+
UNION ALL
|
|
39
|
+
SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
|
|
40
|
+
WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)
|
|
41
|
+
UNION ALL
|
|
42
|
+
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
|
|
43
|
+
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
|
|
44
|
+
UNION ALL
|
|
45
|
+
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
|
|
46
|
+
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
|
|
47
|
+
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 ?
|
|
48
|
+
UNION ALL
|
|
49
|
+
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
|
|
50
|
+
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')
|
|
51
|
+
UNION ALL
|
|
52
|
+
SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
|
|
53
|
+
FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
|
|
54
|
+
UNION ALL
|
|
55
|
+
SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
|
|
56
|
+
FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
|
|
57
|
+
UNION ALL
|
|
58
|
+
SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
|
|
59
|
+
WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
|
|
60
|
+
UNION ALL
|
|
61
|
+
SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
|
|
62
|
+
WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
|
|
63
|
+
UNION ALL
|
|
64
|
+
SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
|
|
65
|
+
FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`,
|
|
66
|
+
).all(strict, strict, strict, strict, strict, strict, strict) as Diagnostic[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function schemaDriftDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
70
|
+
if (!strict) return [];
|
|
71
|
+
const columns = db.prepare('PRAGMA table_info(symbols)').all() as Array<{ name?: string }>;
|
|
72
|
+
const legacy = columns.filter((row) => ['external_target_kind', 'external_target_id', 'external_target_label', 'external_target_dynamic'].includes(String(row.name))).map((row) => row.name);
|
|
73
|
+
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() as Diagnostic[];
|
|
74
|
+
const out: Diagnostic[] = [];
|
|
75
|
+
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' });
|
|
76
|
+
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' });
|
|
77
|
+
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.' });
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function analyzerVersionDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
82
|
+
if (!strict) return [];
|
|
83
|
+
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 Diagnostic[];
|
|
84
|
+
if (rows.length === 0) return [];
|
|
85
|
+
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' }];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function localServiceDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
89
|
+
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 }>;
|
|
90
|
+
const implementationContext = rows.filter((row) => row.status === 'resolved' && String(row.evidenceJson ?? '').includes('implementation_context_caller_ownership')).length;
|
|
91
|
+
const withoutOwnership = rows.filter((row) => row.reason === 'local_service_candidate_without_caller_ownership' || String(row.evidenceJson ?? '').includes('local_service_candidate_without_caller_ownership')).length;
|
|
92
|
+
const unresolved = rows.filter((row) => row.status === 'unresolved').length;
|
|
93
|
+
const outsideScope = rows.filter((row) => row.status === 'unresolved' && candidateCount(row.evidenceJson) > 0).length;
|
|
94
|
+
const out: Diagnostic[] = [];
|
|
95
|
+
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}` });
|
|
96
|
+
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}` });
|
|
97
|
+
if (strict && unresolved > 0) out.push({ severity: 'warning', code: 'local_service_calls_unresolved', message: `Unresolved local service calls: ${unresolved}` });
|
|
98
|
+
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}` });
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parserQualityDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
103
|
+
if (!strict) return [];
|
|
104
|
+
const symbol = symbolCallQuality(db);
|
|
105
|
+
const dbq = dbQueryQuality(db);
|
|
106
|
+
const outbound = outboundOwnershipQuality(db);
|
|
107
|
+
return [
|
|
108
|
+
identityAliasBindingQuality(db),
|
|
109
|
+
remoteActionNoBindingQuality(db),
|
|
110
|
+
contextualImplementationQuality(db),
|
|
111
|
+
implementationCandidateQuality(db),
|
|
112
|
+
classInstanceNoiseQuality(db),
|
|
113
|
+
contextualBindingPropagationQuality(db),
|
|
114
|
+
nestedThisReceiverQuality(db),
|
|
115
|
+
wrapperPathPropagationQuality(db),
|
|
116
|
+
remoteQueryTargetQuality(db),
|
|
117
|
+
remoteEntityOperationCollisionQuality(db),
|
|
118
|
+
remoteEntityDynamicOperationFalsePositiveQuality(db),
|
|
119
|
+
remoteActionTargetQuality(db),
|
|
120
|
+
externalHttpTargetQuality(db),
|
|
121
|
+
odataInvocationResolutionQuality(db),
|
|
122
|
+
...jsonEvidenceQuality(db),
|
|
123
|
+
eventReceiverQuality(db),
|
|
124
|
+
graphDynamicFlagQuality(db),
|
|
125
|
+
symbol,
|
|
126
|
+
dbq,
|
|
127
|
+
outbound,
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function jsonEvidenceQuality(db: Db): Diagnostic[] {
|
|
132
|
+
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() as Record<string, unknown>;
|
|
133
|
+
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() as Record<string, unknown>;
|
|
134
|
+
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() as Record<string, unknown>;
|
|
135
|
+
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() as Diagnostic[];
|
|
136
|
+
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() as Diagnostic[];
|
|
137
|
+
return [
|
|
138
|
+
{ 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) },
|
|
139
|
+
{ 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 },
|
|
140
|
+
{ 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 },
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function symbolCallQuality(db: Db): Diagnostic {
|
|
145
|
+
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() as Record<string, unknown>;
|
|
146
|
+
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 Diagnostic[];
|
|
147
|
+
const total = Number(row.total ?? 0);
|
|
148
|
+
const unresolved = Number(row.unresolved ?? 0);
|
|
149
|
+
const ratio = total === 0 ? 0 : Number((unresolved / total).toFixed(4));
|
|
150
|
+
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 };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function dbQueryQuality(db: Db): Diagnostic {
|
|
154
|
+
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() as Record<string, unknown>;
|
|
155
|
+
const total = Number(row.total ?? 0);
|
|
156
|
+
const unknown = Number(row.unknown ?? 0);
|
|
157
|
+
const ratio = total === 0 ? 0 : Number((unknown / total).toFixed(4));
|
|
158
|
+
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 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function outboundOwnershipQuality(db: Db): Diagnostic {
|
|
162
|
+
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() as Record<string, unknown>;
|
|
163
|
+
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() as Diagnostic[];
|
|
164
|
+
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() as Diagnostic[];
|
|
165
|
+
const total = Number(row.total ?? 0);
|
|
166
|
+
const without = Number(row.withoutOwnership ?? 0);
|
|
167
|
+
const ratio = total === 0 ? 0 : Number((without / total).toFixed(4));
|
|
168
|
+
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 };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function eventReceiverQuality(db: Db): Diagnostic {
|
|
172
|
+
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() as Record<string, unknown>;
|
|
173
|
+
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) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function graphDynamicFlagQuality(db: Db): Diagnostic {
|
|
177
|
+
const row = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get() as Record<string, unknown>;
|
|
178
|
+
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) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function implementationCandidateQuality(db: Db): Diagnostic {
|
|
182
|
+
const categories = [...implementationEdgeCategories(db), missingParameterMetadataCategory(db)].filter((item) => item.count > 0);
|
|
183
|
+
const total = categories.reduce((sum, item) => sum + item.count, 0);
|
|
184
|
+
return { severity: total > 0 ? 'warning' : 'info', code: 'strict_implementation_candidate_quality', message: 'Implementation candidate ambiguity and rejection aggregate', total, categories };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function implementationEdgeCategories(db: Db): Array<Diagnostic & { count: number }> {
|
|
188
|
+
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
|
|
189
|
+
FROM graph_edges e JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
|
|
190
|
+
JOIN cds_services s ON s.id=o.service_id LEFT JOIN cds_operations base ON base.id=o.base_operation_id
|
|
191
|
+
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() as Diagnostic[];
|
|
192
|
+
const grouped = new Map<string, Diagnostic & { count: number; servicePaths: string[]; examples: Diagnostic[] }>();
|
|
193
|
+
for (const row of rows) addImplementationCategory(grouped, row);
|
|
194
|
+
return [...grouped.values()].map(({ servicePaths, ...item }) => ({ ...item, servicePathPattern: pathPattern(servicePaths), examples: item.examples.slice(0, 3) }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function addImplementationCategory(grouped: Map<string, Diagnostic & { count: number; servicePaths: string[]; examples: Diagnostic[] }>, row: Diagnostic): void {
|
|
198
|
+
const evidence = parseObject(row.evidenceJson);
|
|
199
|
+
const category = implementationCategory(row, evidence);
|
|
200
|
+
const family = candidateFamily(evidence);
|
|
201
|
+
const reason = category === 'duplicate_package_name_candidates' ? 'duplicate_package_name_candidates' : String(row.unresolvedReason ?? category);
|
|
202
|
+
const baseOperation = String(row.baseOperation ?? row.operationName ?? evidence.operationName ?? 'unknown');
|
|
203
|
+
const key = [category, baseOperation, reason, family].join('\0');
|
|
204
|
+
const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
|
|
205
|
+
current.count += 1;
|
|
206
|
+
current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ''));
|
|
207
|
+
current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason });
|
|
208
|
+
grouped.set(key, current);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function missingParameterMetadataCategory(db: Db): Diagnostic & { count: number } {
|
|
212
|
+
const examples = db.prepare(`SELECT sc.source_file sourceFile,sc.source_line sourceLine,sc.callee_expression calleeExpression
|
|
213
|
+
FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
|
|
214
|
+
WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
|
|
215
|
+
AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)
|
|
216
|
+
ORDER BY sc.source_file,sc.source_line LIMIT 3`).all() as Diagnostic[];
|
|
217
|
+
const row = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
|
|
218
|
+
WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
|
|
219
|
+
AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get() as { count?: number };
|
|
220
|
+
return { category: 'missing_parameter_metadata', reason: 'callee symbol is missing parameter binding metadata', candidateFamily: 'symbol_parameter_metadata', count: Number(row.count ?? 0), examples };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function implementationCategory(row: Diagnostic, evidence: Diagnostic): string {
|
|
224
|
+
const reasons = JSON.stringify([evidence.ambiguityReasons, evidence.candidateFamilies, evidence.candidates, row.unresolvedReason]);
|
|
225
|
+
if (reasons.includes('duplicate_package_name_candidates')) return 'duplicate_package_name_candidates';
|
|
226
|
+
if (reasons.includes('missing direct ownership')) return 'missing_strong_ownership_evidence';
|
|
227
|
+
return String(row.status) === 'ambiguous' ? 'ambiguous_implementation_candidates' : 'rejected_implementation_candidates';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function candidateFamily(evidence: Diagnostic): string {
|
|
231
|
+
const families = asRecords(evidence.candidateFamilies);
|
|
232
|
+
const duplicate = families.find((row) => typeof row.packageName === 'string');
|
|
233
|
+
if (duplicate?.packageName) return String(duplicate.packageName);
|
|
234
|
+
const candidates = asRecords(evidence.candidates);
|
|
235
|
+
const first = candidates.find((row) => parseObject(row.handlerPackage).packageName);
|
|
236
|
+
return String(parseObject(first?.handlerPackage).packageName ?? 'unknown');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function pathPattern(paths: string[]): string {
|
|
240
|
+
const values = [...new Set(paths.filter(Boolean))].sort();
|
|
241
|
+
if (values.length <= 1) return values[0] ?? '';
|
|
242
|
+
const prefix = commonPrefix(values);
|
|
243
|
+
const suffix = commonSuffix(values.map((value) => value.slice(prefix.length)));
|
|
244
|
+
return `${prefix}*${suffix}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function commonPrefix(values: string[]): string {
|
|
248
|
+
let prefix = values[0] ?? '';
|
|
249
|
+
for (const value of values.slice(1)) while (!value.startsWith(prefix)) prefix = prefix.slice(0, -1);
|
|
250
|
+
return prefix;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function commonSuffix(values: string[]): string {
|
|
254
|
+
let suffix = values[0] ?? '';
|
|
255
|
+
for (const value of values.slice(1)) while (!value.endsWith(suffix)) suffix = suffix.slice(1);
|
|
256
|
+
return suffix;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function contextualImplementationQuality(db: Db): Diagnostic {
|
|
260
|
+
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() as Diagnostic[];
|
|
261
|
+
const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
|
|
262
|
+
return { severity: total > 0 ? 'warning' : 'info', code: 'strict_contextual_implementation_quality', message: 'Implementation hops stopped by ambiguous or unresolved implementation edges', total, breakdown: rows };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function classInstanceNoiseQuality(db: Db): Diagnostic {
|
|
266
|
+
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'];
|
|
267
|
+
const placeholders = builtIns.map(() => '?').join(',');
|
|
268
|
+
const aggregate = db.prepare(`SELECT COUNT(*) total,
|
|
269
|
+
SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
|
|
270
|
+
SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
|
|
271
|
+
FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns) as Diagnostic;
|
|
272
|
+
const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
|
|
273
|
+
FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
|
|
274
|
+
GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all() as Diagnostic[];
|
|
275
|
+
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 };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function contextualBindingPropagationQuality(db: Db): Diagnostic {
|
|
279
|
+
const missing = missingParameterMetadataCategory(db);
|
|
280
|
+
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() as Diagnostic[];
|
|
281
|
+
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 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function wrapperPathPropagationQuality(db: Db): Diagnostic {
|
|
285
|
+
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() as Diagnostic[];
|
|
286
|
+
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 };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function nestedThisReceiverQuality(db: Db): Diagnostic {
|
|
290
|
+
const aggregate = db.prepare(`SELECT COUNT(*) total,
|
|
291
|
+
SUM(CASE WHEN json_extract(evidence_json,'$.relation')='indexed_this_method' THEN 1 ELSE 0 END) resolvedToCurrentClass,
|
|
292
|
+
SUM(CASE WHEN json_extract(evidence_json,'$.relation')='class_instance_method' THEN 1 ELSE 0 END) withExplicitHelperInstanceEvidence
|
|
293
|
+
FROM symbol_calls WHERE callee_expression LIKE 'this.%.%'`).get() as Diagnostic;
|
|
294
|
+
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
|
|
295
|
+
FROM symbol_calls WHERE callee_expression LIKE 'this.%.%' AND json_extract(evidence_json,'$.relation')='indexed_this_method'
|
|
296
|
+
ORDER BY source_file,source_line LIMIT 8`).all() as Diagnostic[];
|
|
297
|
+
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 };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function remoteQueryTargetQuality(db: Db): Diagnostic {
|
|
301
|
+
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() as Diagnostic;
|
|
302
|
+
const numeric = Number(row.numericTargets ?? 0);
|
|
303
|
+
const unresolved = Number(row.unresolved ?? 0);
|
|
304
|
+
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 };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function remoteEntityOperationCollisionQuality(db: Db): Diagnostic {
|
|
308
|
+
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
|
|
309
|
+
FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
|
|
310
|
+
WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
|
|
311
|
+
ORDER BY c.source_file,c.source_line LIMIT 100`).all() as Diagnostic[];
|
|
312
|
+
const examples = rows.map((row) => operationCollisionExample(db, row)).filter((row): row is Diagnostic => Boolean(row)).slice(0, 10);
|
|
313
|
+
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 };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function operationCollisionExample(db: Db, row: Diagnostic): Diagnostic | undefined {
|
|
317
|
+
const rawPath = String(row.rawPath ?? '');
|
|
318
|
+
const normalized = normalizeODataOperationInvocationPath(rawPath);
|
|
319
|
+
const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
|
|
320
|
+
const name = candidatePath.replace(/^\//, '');
|
|
321
|
+
const simple = name.split('.').at(-1) ?? name;
|
|
322
|
+
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 };
|
|
323
|
+
const candidateCount = Number(candidates.count ?? 0);
|
|
324
|
+
if (!normalized?.wasInvocation && candidateCount === 0) return undefined;
|
|
325
|
+
const evidence = parseObject(row.evidenceJson);
|
|
326
|
+
return { 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: parseObject(evidence.odataPathIntent).reason };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function remoteEntityDynamicOperationFalsePositiveQuality(db: Db): Diagnostic {
|
|
330
|
+
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
|
|
331
|
+
FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
|
|
332
|
+
WHERE c.call_type LIKE 'remote_entity_%' AND e.status IN ('dynamic','unresolved') AND e.to_kind='operation_candidate'
|
|
333
|
+
ORDER BY c.source_file,c.source_line LIMIT 100`).all() as Diagnostic[];
|
|
334
|
+
const examples = rows.filter(isRemoteEntityFalsePositive).map((row) => falsePositiveExample(row)).slice(0, 10);
|
|
335
|
+
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 };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isRemoteEntityFalsePositive(row: Diagnostic): boolean {
|
|
339
|
+
const intent = classifyODataPathIntent(String(row.rawPath ?? ''), String(row.method ?? 'GET'));
|
|
340
|
+
const entityIntent = ['entity_key_read', 'entity_navigation_query', 'entity_media'].includes(intent.kind) || (intent.kind === 'entity_mutation' && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix));
|
|
341
|
+
const evidence = parseObject(row.evidenceJson);
|
|
342
|
+
const candidateCount = Number(evidence.indexedOperationCandidateCount ?? evidence.candidateCount ?? 0);
|
|
343
|
+
const reason = String(row.unresolvedReason ?? '');
|
|
344
|
+
return entityIntent && candidateCount === 0 && (intent.keyPredicatePlaceholderKeys.length > 0 || reason.includes('runtime variable') || reason.includes('placeholder'));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function falsePositiveExample(row: Diagnostic): Diagnostic {
|
|
348
|
+
const intent = classifyODataPathIntent(String(row.rawPath ?? ''), String(row.method ?? 'GET'));
|
|
349
|
+
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.' };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function remoteActionTargetQuality(db: Db): Diagnostic {
|
|
353
|
+
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() as Diagnostic;
|
|
354
|
+
const numeric = Number(row.numericTargets ?? 0);
|
|
355
|
+
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) };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function externalHttpTargetQuality(db: Db): Diagnostic {
|
|
359
|
+
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() as Diagnostic;
|
|
360
|
+
const bad = Number(row.numericTargets ?? 0) + Number(row.invalidEvidence ?? 0);
|
|
361
|
+
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) };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function odataInvocationResolutionQuality(db: Db): Diagnostic {
|
|
365
|
+
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() as Array<{ operationPathExpr?: string; status?: string }>;
|
|
366
|
+
const unresolved = rows.filter((row) => row.status === 'unresolved' && normalizeODataOperationInvocationPath(row.operationPathExpr)?.wasInvocation).length;
|
|
367
|
+
const ambiguous = rows.filter((row) => row.status === 'ambiguous' && normalizeODataOperationInvocationPath(row.operationPathExpr)?.wasInvocation).length;
|
|
368
|
+
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 };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function identityAliasBindingQuality(db: Db): Diagnostic {
|
|
372
|
+
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() as Diagnostic[];
|
|
373
|
+
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 };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function remoteActionNoBindingQuality(db: Db): Diagnostic {
|
|
377
|
+
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() as Diagnostic[];
|
|
378
|
+
const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
|
|
379
|
+
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 };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function candidateCount(value: unknown): number {
|
|
383
|
+
return Number(parseObject(value).candidateCount ?? 0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseObject(value: unknown): Diagnostic {
|
|
387
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value as Diagnostic;
|
|
388
|
+
try {
|
|
389
|
+
const parsed = JSON.parse(String(value ?? '{}')) as unknown;
|
|
390
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Diagnostic : {};
|
|
391
|
+
} catch {
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function asRecords(value: unknown): Diagnostic[] {
|
|
397
|
+
return Array.isArray(value) ? value.filter((item): item is Diagnostic => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
398
|
+
}
|