@saptools/service-flow 0.1.52 → 0.1.54
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 +12 -0
- package/README.md +11 -12
- package/TECHNICAL-NOTE.md +18 -3
- package/dist/{chunk-PTLDSHRC.js → chunk-ERIZHM5C.js} +2646 -1067
- package/dist/chunk-ERIZHM5C.js.map +1 -0
- package/dist/cli.js +162 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-doctor-projection.ts +136 -0
- package/src/cli/doctor.ts +20 -3
- package/src/db/repositories.ts +10 -2
- package/src/linker/000-implementation-candidates.ts +674 -0
- package/src/linker/001-implementation-evidence-projection.ts +191 -0
- package/src/linker/002-call-evidence.ts +226 -0
- package/src/linker/cross-repo-linker.ts +27 -453
- package/src/linker/dynamic-edge-resolver.ts +35 -0
- package/src/linker/external-http-target.ts +24 -3
- package/src/linker/helper-package-linker.ts +18 -2
- package/src/linker/service-resolver.ts +45 -2
- package/src/output/doctor-output.ts +13 -4
- package/src/output/table-output.ts +15 -4
- package/src/trace/000-dynamic-target-types.ts +12 -0
- package/src/trace/001-dynamic-identity.ts +45 -17
- package/src/trace/003-dynamic-references.ts +236 -35
- package/src/trace/004-dynamic-candidate-sources.ts +155 -0
- package/src/trace/005-implementation-selection.ts +187 -0
- package/src/trace/006-contextual-projection.ts +30 -0
- package/src/trace/007-implementation-start-diagnostic.ts +61 -0
- package/src/trace/008-contextual-runtime-state.ts +294 -0
- package/src/trace/009-selected-handler-provenance.ts +186 -0
- package/src/trace/dynamic-targets.ts +205 -159
- package/src/trace/evidence.ts +132 -34
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +74 -9
- package/src/trace/trace-engine.ts +97 -125
- package/src/utils/000-bounded-projection.ts +166 -0
- package/dist/chunk-PTLDSHRC.js.map +0 -1
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
2
|
import { reposByName } from '../db/repositories.js';
|
|
3
3
|
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
4
|
-
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
5
|
-
import { resolveOperation } from '../linker/service-resolver.js';
|
|
6
4
|
import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
|
|
7
5
|
import { baseTraceEvidence, edgeTarget, runtimeNoCandidateDiagnostics, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
8
6
|
import { dynamicCandidateBranches } from './dynamic-branches.js';
|
|
@@ -10,7 +8,22 @@ import {
|
|
|
10
8
|
loadTraceDiagnostics,
|
|
11
9
|
prependTraceDiagnostic,
|
|
12
10
|
} from './002-trace-diagnostics.js';
|
|
13
|
-
import { implementationHintDiagnostic
|
|
11
|
+
import { implementationHintDiagnostic } from './implementation-hints.js';
|
|
12
|
+
import {
|
|
13
|
+
contextualImplementationSelection,
|
|
14
|
+
hintedImplementationSelection,
|
|
15
|
+
} from './005-implementation-selection.js';
|
|
16
|
+
import { implementationStartDiagnostic } from './007-implementation-start-diagnostic.js';
|
|
17
|
+
import {
|
|
18
|
+
contextualRuntimeResolution,
|
|
19
|
+
type ContextBinding,
|
|
20
|
+
} from './008-contextual-runtime-state.js';
|
|
21
|
+
import {
|
|
22
|
+
handlerMethodNode,
|
|
23
|
+
withSelectedHandlerProvenance,
|
|
24
|
+
type SelectedHandlerEvidence,
|
|
25
|
+
} from './009-selected-handler-provenance.js';
|
|
26
|
+
import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
|
|
14
27
|
import {
|
|
15
28
|
ambiguousStartDiagnostic,
|
|
16
29
|
selectorNotFoundDiagnostic,
|
|
@@ -53,32 +66,6 @@ interface GraphRow extends Record<string, unknown> {
|
|
|
53
66
|
unresolved_reason?: string;
|
|
54
67
|
status?: string;
|
|
55
68
|
}
|
|
56
|
-
interface ContextBinding {
|
|
57
|
-
bindingId?: number;
|
|
58
|
-
alias?: string;
|
|
59
|
-
aliasExpr?: string;
|
|
60
|
-
destinationExpr?: string;
|
|
61
|
-
servicePathExpr?: string;
|
|
62
|
-
requireServicePath?: string;
|
|
63
|
-
requireDestination?: string;
|
|
64
|
-
effectiveServicePath?: string;
|
|
65
|
-
effectiveDestination?: string;
|
|
66
|
-
sourceFile?: string;
|
|
67
|
-
sourceLine?: number;
|
|
68
|
-
source: string;
|
|
69
|
-
callerArgument?: string;
|
|
70
|
-
callerProperty?: string;
|
|
71
|
-
calleeParameter?: string;
|
|
72
|
-
calleeObjectProperty?: string;
|
|
73
|
-
calleeLocalDestructuredIdentifier?: string;
|
|
74
|
-
parameterPropertyAliasKind?: unknown;
|
|
75
|
-
parameterPropertyAliasLine?: unknown;
|
|
76
|
-
calleeReceiver: string;
|
|
77
|
-
callerSite?: { sourceFile?: string; sourceLine?: number };
|
|
78
|
-
calleeSite?: { sourceFile?: string; sourceLine?: number };
|
|
79
|
-
resolutionStatus?: 'selected' | 'ambiguous';
|
|
80
|
-
bindingCandidates?: Array<Record<string, unknown>>;
|
|
81
|
-
}
|
|
82
69
|
interface ImplementationHintOptions {
|
|
83
70
|
implementationRepo?: string;
|
|
84
71
|
implementationHints?: ImplementationHint[];
|
|
@@ -110,31 +97,21 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
|
|
|
110
97
|
const operationId = String(rows[0]?.operationId);
|
|
111
98
|
const impl = implementationScope(db, operationId);
|
|
112
99
|
if (impl.edge?.status === 'resolved' && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? new Set([impl.symbolId]) : undefined, repoId: impl.repoId, operationId, diagnostics: [] };
|
|
113
|
-
const hinted =
|
|
100
|
+
const hinted = hintedImplementationSelection(
|
|
101
|
+
db, impl.edge, operationId, hintOptions,
|
|
102
|
+
);
|
|
114
103
|
if (hinted.methodId) {
|
|
115
104
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
116
105
|
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, repoId: hintedScope.repoId, operationId, diagnostics: [] };
|
|
117
106
|
}
|
|
118
107
|
if (impl.edge) {
|
|
119
108
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
120
|
-
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence
|
|
121
|
-
const diagnostics = [
|
|
109
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence);
|
|
110
|
+
const diagnostics = [implementationStartDiagnostic(impl.edge, evidence)];
|
|
122
111
|
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
123
112
|
}
|
|
124
113
|
return { operationId, diagnostics: [{ severity: 'warning', code: 'trace_start_implementation_unresolved', message: 'Indexed operation matched but no implementation candidate exists', resolutionStage: 'implementation', resolutionStatus: 'operation_without_implementation' }] };
|
|
125
114
|
}
|
|
126
|
-
function implementationRejectionReasons(evidence: Record<string, unknown>): string[] {
|
|
127
|
-
const candidates = Array.isArray(evidence.candidates)
|
|
128
|
-
? evidence.candidates.filter((item): item is Record<string, unknown> =>
|
|
129
|
-
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
130
|
-
: [];
|
|
131
|
-
const reasons = candidates.flatMap((candidate) =>
|
|
132
|
-
Array.isArray(candidate.rejectedReasons)
|
|
133
|
-
? candidate.rejectedReasons.filter((reason): reason is string =>
|
|
134
|
-
typeof reason === 'string')
|
|
135
|
-
: []);
|
|
136
|
-
return [...new Set(reasons)].sort();
|
|
137
|
-
}
|
|
138
115
|
function sourceFilesForStart(
|
|
139
116
|
db: Db,
|
|
140
117
|
repoId: number | undefined,
|
|
@@ -222,11 +199,6 @@ function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
|
|
|
222
199
|
function implementationEdge(db: Db, operationId: string): GraphRow | undefined {
|
|
223
200
|
return db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1").get(operationId) as GraphRow | undefined;
|
|
224
201
|
}
|
|
225
|
-
function handlerMethodNode(db: Db, methodId: string): Record<string, unknown> | undefined {
|
|
226
|
-
const row = db.prepare(`SELECT hm.id methodId,hm.method_name methodName,hm.decorator_value decoratorValue,hm.source_line sourceLine,hc.class_name className,hc.source_file sourceFile,r.name repoName,r.id repoId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(methodId) as Record<string, unknown> | undefined;
|
|
227
|
-
if (!row) return undefined;
|
|
228
|
-
return { id: `handler_method:${methodId}`, kind: 'handler_method', label: `${String(row.repoName)}:${String(row.className)}.${String(row.methodName)}`, ...row };
|
|
229
|
-
}
|
|
230
202
|
function implementationScope(db: Db, operationId: string): { repoId?: number; files: Set<string>; symbolId?: number; edge?: GraphRow } {
|
|
231
203
|
const edge = implementationEdge(db, operationId);
|
|
232
204
|
if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
|
|
@@ -235,29 +207,6 @@ function implementationScope(db: Db, operationId: string): { repoId?: number; fi
|
|
|
235
207
|
return { repoId: row?.repoId, files: new Set(), edge };
|
|
236
208
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
237
209
|
}
|
|
238
|
-
function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
|
|
239
|
-
if (!edge || edge.status !== 'ambiguous') return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
|
|
240
|
-
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
241
|
-
}
|
|
242
|
-
function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
|
|
243
|
-
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
244
|
-
if (hinted.methodId) return hinted;
|
|
245
|
-
if (hinted.blocksAutomatic) return hinted;
|
|
246
|
-
if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
|
|
247
|
-
const evidence = JSON.parse(String(edge.evidence_json || '{}')) as { candidates?: Array<{ accepted?: boolean; methodId?: number; handlerPackage?: { id?: number; name?: string }; applicationPackage?: { id?: number; name?: string }; reasons?: string[]; score?: number }> };
|
|
248
|
-
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
249
|
-
const reasons: string[] = [];
|
|
250
|
-
let score = Number(item.score ?? 0);
|
|
251
|
-
if (Number(item.handlerPackage?.id) === callerRepoId) { score += 10; reasons.push('handler_package_matches_caller_repository'); }
|
|
252
|
-
if (Number(item.applicationPackage?.id) === callerRepoId) { score += 10; reasons.push('registration_package_matches_caller_repository'); }
|
|
253
|
-
if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
|
|
254
|
-
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
255
|
-
}).sort((a, b) => b.score - a.score);
|
|
256
|
-
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
257
|
-
const [first, second] = scores;
|
|
258
|
-
if (first && first.methodId !== undefined && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), blocksAutomatic: false, evidence: { status: 'selected', selectedMethodId: first.methodId, candidateScores: scores } };
|
|
259
|
-
return { blocksAutomatic: false, evidence: hinted.evidence.reason === 'no_scoped_hint_matched_edge' ? hinted.evidence : { status: 'tied', tieReason: scores.length > 1 ? 'duplicate_helper_implementation_candidates' : 'no_unique_materially_stronger_candidate', candidateScores: scores } };
|
|
260
|
-
}
|
|
261
210
|
function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
|
|
262
211
|
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.qualified_name=hc.class_name || '.' || hm.method_name AND s.start_line=hm.source_line WHERE hm.id=?").get(methodId) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
263
212
|
if (!row || typeof row.symbolId !== 'number') return undefined;
|
|
@@ -317,11 +266,15 @@ function workspaceIdForCall(db: Db, callId: string): number | undefined {
|
|
|
317
266
|
function parseEvidence(value: unknown): Record<string, unknown> {
|
|
318
267
|
try {
|
|
319
268
|
const parsed = JSON.parse(String(value || '{}')) as unknown;
|
|
320
|
-
return
|
|
269
|
+
return isEvidenceRecord(parsed) ? boundCandidateLikeEvidence(parsed) : {};
|
|
321
270
|
} catch {
|
|
322
271
|
return {};
|
|
323
272
|
}
|
|
324
273
|
}
|
|
274
|
+
|
|
275
|
+
function isEvidenceRecord(value: unknown): value is Record<string, unknown> {
|
|
276
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
277
|
+
}
|
|
325
278
|
function receiverFromEvidence(value: unknown): string | undefined {
|
|
326
279
|
const evidence = parseEvidence(value);
|
|
327
280
|
return typeof evidence.receiver === 'string' ? evidence.receiver : undefined;
|
|
@@ -458,35 +411,6 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
458
411
|
});
|
|
459
412
|
return next;
|
|
460
413
|
}
|
|
461
|
-
function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBinding | undefined, workspaceId: number | undefined, persistedRows: GraphRow[] = []): { row?: GraphRow; evidence?: Record<string, unknown>; unresolvedReason?: string } {
|
|
462
|
-
if (!binding || String(call.call_type) !== 'remote_action' || call.operation_path_expr === undefined || call.operation_path_expr === null) return {};
|
|
463
|
-
if (binding.resolutionStatus === 'ambiguous') {
|
|
464
|
-
return {
|
|
465
|
-
evidence: {
|
|
466
|
-
contextualServiceBindingAttempted: true,
|
|
467
|
-
contextualBinding: {
|
|
468
|
-
source: binding.source,
|
|
469
|
-
status: 'tied',
|
|
470
|
-
candidates: binding.bindingCandidates,
|
|
471
|
-
},
|
|
472
|
-
contextualResolutionStatus: 'ambiguous',
|
|
473
|
-
contextualCandidateCount: binding.bindingCandidates?.length ?? 0,
|
|
474
|
-
},
|
|
475
|
-
unresolvedReason: 'Ambiguous contextual service binding candidates',
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
const normalized = normalizeODataOperationInvocationPath(String(call.operation_path_expr));
|
|
479
|
-
const op = normalized?.normalizedOperationPath ?? (String(call.operation_path_expr).startsWith('/') ? String(call.operation_path_expr) : `/${String(call.operation_path_expr)}`);
|
|
480
|
-
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
481
|
-
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
482
|
-
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
483
|
-
const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, callerSite: binding.callerSite, calleeSite: binding.calleeSite, bindingSourceFile: binding.sourceFile, bindingSourceLine: binding.sourceLine, alias: binding.alias, aliasExpr: binding.aliasExpr, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination }, operationPath: op, rawOperationPath: normalized?.rawOperationPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : undefined, invocationArgumentPlaceholderKeys: normalized?.invocationArgumentPlaceholderKeys.length ? normalized.invocationArgumentPlaceholderKeys : undefined, servicePath, serviceAlias: binding.alias, serviceAliasExpr: binding.aliasExpr, destination, requireServicePath: binding.requireServicePath, requireDestination: binding.requireDestination, effectiveServicePath: binding.effectiveServicePath, effectiveDestination: binding.effectiveDestination, contextualResolutionStatus: resolution.status, contextualCandidateCount: resolution.candidates.length, candidates: resolution.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
484
|
-
if (!resolution.target) return { evidence, unresolvedReason: resolution.status === 'ambiguous' ? 'Ambiguous contextual operation candidates' : resolution.status === 'dynamic' ? `Dynamic contextual target is missing runtime variables: ${resolution.reasons.join(', ')}` : 'No contextual operation candidate matched' };
|
|
485
|
-
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
486
|
-
const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
|
|
487
|
-
if (persistedResolved) return { row: undefined, evidence: { ...resolvedEvidence, contextualPreservedPersistedResolvedEdge: true }, unresolvedReason: undefined };
|
|
488
|
-
return { row: { id: -Number(call.id), edge_type: 'REMOTE_CALL_RESOLVES_TO_OPERATION', from_id: String(call.id), to_kind: 'operation', to_id: String(resolution.target.operationId), confidence: resolution.target.score, evidence_json: JSON.stringify(resolvedEvidence), status: 'resolved' }, evidence: resolvedEvidence, unresolvedReason: undefined };
|
|
489
|
-
}
|
|
490
414
|
export function trace(
|
|
491
415
|
db: Db,
|
|
492
416
|
start: TraceStart,
|
|
@@ -524,14 +448,32 @@ export function trace(
|
|
|
524
448
|
const op = operationNode(db, scope.startOperationId);
|
|
525
449
|
const impl = implementationScope(db, scope.startOperationId);
|
|
526
450
|
if (op) nodes.set(String(op.id), op);
|
|
527
|
-
const startSelection =
|
|
451
|
+
const startSelection = hintedImplementationSelection(
|
|
452
|
+
db, impl.edge, scope.startOperationId, hintOptions,
|
|
453
|
+
);
|
|
528
454
|
if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
|
|
529
455
|
const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
|
|
530
|
-
const implEvidence = {
|
|
531
|
-
|
|
532
|
-
|
|
456
|
+
const implEvidence = {
|
|
457
|
+
...parseEvidence(impl.edge.evidence_json),
|
|
458
|
+
startResolution: {
|
|
459
|
+
strategy: 'indexed_operation_graph',
|
|
460
|
+
matchedOperationId: scope.startOperationId,
|
|
461
|
+
implementationEdgeId: impl.edge.id,
|
|
462
|
+
implementationStatus: impl.edge.status,
|
|
463
|
+
selectedHandlerMethodId: selectedMethodId,
|
|
464
|
+
},
|
|
465
|
+
implementationSelection: startSelection.methodId
|
|
466
|
+
? startSelection.evidence : undefined,
|
|
467
|
+
};
|
|
468
|
+
const selected: SelectedHandlerEvidence = selectedMethodId
|
|
469
|
+
? withSelectedHandlerProvenance(
|
|
470
|
+
implEvidence, selectedMethodId, handlerMethodNode(db, selectedMethodId),
|
|
471
|
+
)
|
|
472
|
+
: { evidence: implEvidence };
|
|
473
|
+
if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
|
|
474
|
+
if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
|
|
533
475
|
seenEdges.add(Number(impl.edge.id));
|
|
534
|
-
edges.push({ step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to:
|
|
476
|
+
edges.push({ step: 1, type: 'operation_implemented_by_handler', from: op?.label ? String(op.label) : `operation:${scope.startOperationId}`, to: selected.handler?.label ? String(selected.handler.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: selected.evidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: selected.unresolvedReason ?? (impl.edge.status === 'resolved' || startSelection.methodId ? undefined : String(impl.edge.unresolved_reason ?? impl.edge.status)) });
|
|
535
477
|
}
|
|
536
478
|
}
|
|
537
479
|
const seenScopes = new Set<string>();
|
|
@@ -564,7 +506,7 @@ export function trace(
|
|
|
564
506
|
const nextKey = `${nextRepoId}:${[...nextSymbols].join(',')}:${[...nextFiles].join(',')}`;
|
|
565
507
|
const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
|
|
566
508
|
if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
|
|
567
|
-
const evidence = { ...(
|
|
509
|
+
const evidence = { ...parseEvidence(symbolCall.evidence_json), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
|
|
568
510
|
edges.push({ step: current.depth, type: 'local_symbol_call', from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === 'resolved' ? undefined : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : undefined });
|
|
569
511
|
if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: 'cycle', from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: 'Cycle detected; downstream symbol already visited' });
|
|
570
512
|
else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
|
|
@@ -590,15 +532,13 @@ export function trace(
|
|
|
590
532
|
for (const row of graphRows) {
|
|
591
533
|
if (seenEdges.has(Number(row.id))) continue;
|
|
592
534
|
seenEdges.add(Number(row.id));
|
|
593
|
-
const persistedEvidence =
|
|
594
|
-
String(row.evidence_json || '{}'),
|
|
595
|
-
) as Record<string, unknown>;
|
|
535
|
+
const persistedEvidence = parseEvidence(row.evidence_json);
|
|
596
536
|
const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
|
|
597
537
|
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
|
|
598
538
|
vars: options.vars,
|
|
599
539
|
dynamicMode: options.dynamicMode ?? 'strict',
|
|
600
540
|
maxDynamicCandidates: options.maxDynamicCandidates,
|
|
601
|
-
}, workspaceIdForCall(db, String(call.id)), contextual.
|
|
541
|
+
}, workspaceIdForCall(db, String(call.id)), contextual.state);
|
|
602
542
|
const evidence = effective.evidence;
|
|
603
543
|
const effectiveRow = effective.row;
|
|
604
544
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -618,37 +558,69 @@ export function trace(
|
|
|
618
558
|
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
619
559
|
unresolvedReason: effective.unresolvedReason,
|
|
620
560
|
});
|
|
621
|
-
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
561
|
+
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
562
|
+
&& effectiveRow.status !== 'resolved')
|
|
622
563
|
edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
|
|
623
564
|
if (effectiveRow.to_kind === 'operation') {
|
|
624
565
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
625
|
-
const contextSelection =
|
|
566
|
+
const contextSelection = contextualImplementationSelection(
|
|
567
|
+
db, implementation.edge, effectiveRow.to_id, current.repoId, evidence,
|
|
568
|
+
hintOptions,
|
|
569
|
+
);
|
|
626
570
|
const contextMethodId = contextSelection.methodId;
|
|
627
|
-
|
|
571
|
+
let selectedHandlerAvailable = true;
|
|
628
572
|
if (implementation.edge) {
|
|
629
|
-
const implEvidence =
|
|
630
|
-
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence
|
|
573
|
+
const implEvidence = parseEvidence(implementation.edge.evidence_json);
|
|
574
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence);
|
|
631
575
|
if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
576
|
+
const selectedMethodId = implementation.edge.status === 'resolved'
|
|
577
|
+
? implementation.edge.to_id : contextMethodId;
|
|
578
|
+
const selectionEvidence = contextMethodId
|
|
579
|
+
? {
|
|
580
|
+
...implEvidence,
|
|
581
|
+
contextualImplementationSelected:
|
|
582
|
+
contextSelection.evidence.strategy !== 'implementation_repo_hint',
|
|
583
|
+
contextualImplementation: contextSelection.evidence,
|
|
584
|
+
implementationSelection: contextSelection.evidence,
|
|
585
|
+
}
|
|
586
|
+
: {
|
|
587
|
+
...implEvidence,
|
|
588
|
+
contextualImplementation: contextSelection.evidence,
|
|
589
|
+
implementationSelection: contextSelection.evidence,
|
|
590
|
+
};
|
|
591
|
+
const selected: SelectedHandlerEvidence = selectedMethodId
|
|
592
|
+
? withSelectedHandlerProvenance(
|
|
593
|
+
selectionEvidence,
|
|
594
|
+
selectedMethodId,
|
|
595
|
+
handlerMethodNode(db, selectedMethodId),
|
|
596
|
+
)
|
|
597
|
+
: { evidence: selectionEvidence };
|
|
598
|
+
selectedHandlerAvailable = !selected.unresolvedReason;
|
|
599
|
+
if (selected.diagnostic) prependTraceDiagnostic(diagnostics, selected.diagnostic);
|
|
600
|
+
const implTo = selected.handler?.label
|
|
601
|
+
? String(selected.handler.label)
|
|
602
|
+
: `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
603
|
+
if (selected.handler) nodes.set(String(selected.handler.id), selected.handler);
|
|
635
604
|
edges.push({
|
|
636
605
|
step: current.depth,
|
|
637
606
|
type: 'operation_implemented_by_handler',
|
|
638
607
|
from: to,
|
|
639
608
|
to: implTo,
|
|
640
|
-
evidence:
|
|
641
|
-
? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== 'implementation_repo_hint', contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence }
|
|
642
|
-
: { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
|
|
609
|
+
evidence: selected.evidence,
|
|
643
610
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
644
|
-
unresolvedReason:
|
|
611
|
+
unresolvedReason: selected.unresolvedReason
|
|
612
|
+
?? (implementation.edge.status === 'resolved' || contextMethodId
|
|
613
|
+
? undefined
|
|
614
|
+
: String(implementation.edge.unresolved_reason ?? implementation.edge.status)),
|
|
645
615
|
});
|
|
646
616
|
}
|
|
647
617
|
if (current.depth >= maxDepth) continue;
|
|
648
618
|
const contextScope = contextMethodId ? handlerScope(db, contextMethodId) : undefined;
|
|
649
619
|
const files = contextScope?.files ?? (implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id));
|
|
650
620
|
const symbolIds = contextScope?.symbolId ? new Set([contextScope.symbolId]) : implementation.symbolId ? new Set([implementation.symbolId]) : undefined;
|
|
651
|
-
if (
|
|
621
|
+
if (selectedHandlerAvailable
|
|
622
|
+
&& (implementation.edge?.status === 'resolved' || contextScope)
|
|
623
|
+
&& files.size > 0) {
|
|
652
624
|
const targetRepoId = contextScope?.repoId ?? implementation.repoId ?? (db
|
|
653
625
|
.prepare(
|
|
654
626
|
'SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?',
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export const DEFAULT_EVIDENCE_CANDIDATE_LIMIT = 5;
|
|
2
|
+
|
|
3
|
+
export interface BoundedProjection<T> {
|
|
4
|
+
totalCount: number;
|
|
5
|
+
shownCount: number;
|
|
6
|
+
omittedCount: number;
|
|
7
|
+
items: T[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function projectBounded<T>(
|
|
11
|
+
values: readonly T[],
|
|
12
|
+
compare: (left: T, right: T) => number,
|
|
13
|
+
limit = DEFAULT_EVIDENCE_CANDIDATE_LIMIT,
|
|
14
|
+
): BoundedProjection<T> {
|
|
15
|
+
const normalizedLimit = positiveLimit(limit);
|
|
16
|
+
const sorted = [...values].sort(compare);
|
|
17
|
+
const items = sorted.slice(0, normalizedLimit);
|
|
18
|
+
return {
|
|
19
|
+
totalCount: sorted.length,
|
|
20
|
+
shownCount: items.length,
|
|
21
|
+
omittedCount: Math.max(0, sorted.length - items.length),
|
|
22
|
+
items,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Evidence producers establish semantic order before calling the generic
|
|
28
|
+
* recursive bounder. Re-sorting here would make a selected decision appear
|
|
29
|
+
* to be explained by a different candidate.
|
|
30
|
+
*/
|
|
31
|
+
export function projectBoundedInOrder<T>(
|
|
32
|
+
values: readonly T[],
|
|
33
|
+
limit = DEFAULT_EVIDENCE_CANDIDATE_LIMIT,
|
|
34
|
+
): BoundedProjection<T> {
|
|
35
|
+
const normalizedLimit = positiveLimit(limit);
|
|
36
|
+
const items = values.slice(0, normalizedLimit);
|
|
37
|
+
return {
|
|
38
|
+
totalCount: values.length,
|
|
39
|
+
shownCount: items.length,
|
|
40
|
+
omittedCount: Math.max(0, values.length - items.length),
|
|
41
|
+
items,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function positiveLimit(value: number | undefined): number {
|
|
46
|
+
return Number.isFinite(value) && Number(value) > 0
|
|
47
|
+
? Math.floor(Number(value))
|
|
48
|
+
: DEFAULT_EVIDENCE_CANDIDATE_LIMIT;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const candidateLikeCollections = new Set([
|
|
52
|
+
'candidates',
|
|
53
|
+
'candidateScores',
|
|
54
|
+
'candidateFamilies',
|
|
55
|
+
'candidateEvidence',
|
|
56
|
+
'candidatePaths',
|
|
57
|
+
'candidateRawPaths',
|
|
58
|
+
'candidateNormalizedOperationPaths',
|
|
59
|
+
'normalizedCandidateOperations',
|
|
60
|
+
'candidateLiterals',
|
|
61
|
+
'bindingCandidates',
|
|
62
|
+
'bindingAlternatives',
|
|
63
|
+
'implementationHintSuggestions',
|
|
64
|
+
'selectableImplementationRepositories',
|
|
65
|
+
'matchedHints',
|
|
66
|
+
'candidateSuggestions',
|
|
67
|
+
'dynamicTargetCandidates',
|
|
68
|
+
'dynamicTargetCandidateSuggestions',
|
|
69
|
+
'rejectedCandidates',
|
|
70
|
+
'suggestedVarSets',
|
|
71
|
+
'copyableExamples',
|
|
72
|
+
'selectorSuggestions',
|
|
73
|
+
'serviceSuggestions',
|
|
74
|
+
'repositories',
|
|
75
|
+
'examples',
|
|
76
|
+
'expandedExamples',
|
|
77
|
+
'registrations',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parser facts are retained in their tables; graph evidence only carries a
|
|
82
|
+
* deterministic explanation. This prevents nested parser alternatives from
|
|
83
|
+
* bypassing the graph evidence cap while leaving canonical facts queryable.
|
|
84
|
+
*/
|
|
85
|
+
export function boundCandidateLikeEvidence(
|
|
86
|
+
evidence: Record<string, unknown>,
|
|
87
|
+
limit = DEFAULT_EVIDENCE_CANDIDATE_LIMIT,
|
|
88
|
+
): Record<string, unknown> {
|
|
89
|
+
const output: Record<string, unknown> = {};
|
|
90
|
+
for (const [key, value] of Object.entries(evidence)) {
|
|
91
|
+
if (!Array.isArray(value) || !candidateLikeCollections.has(key)) {
|
|
92
|
+
output[key] = boundNestedEvidence(value, limit);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const projection = projectBoundedInOrder(value, limit);
|
|
96
|
+
output[key] = projection.items.map((item) => boundNestedEvidence(item, limit));
|
|
97
|
+
addCollectionCounts(output, evidence, key, projection);
|
|
98
|
+
}
|
|
99
|
+
return output;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function boundNestedEvidence(value: unknown, limit: number): unknown {
|
|
103
|
+
if (Array.isArray(value)) return value.map((item) => boundNestedEvidence(item, limit));
|
|
104
|
+
if (!isEvidenceRecord(value)) return value;
|
|
105
|
+
return boundCandidateLikeEvidence(value, limit);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isEvidenceRecord(value: unknown): value is Record<string, unknown> {
|
|
109
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function addCollectionCounts(
|
|
113
|
+
output: Record<string, unknown>,
|
|
114
|
+
input: Record<string, unknown>,
|
|
115
|
+
key: string,
|
|
116
|
+
projection: BoundedProjection<unknown>,
|
|
117
|
+
): void {
|
|
118
|
+
const stem = collectionStem(key);
|
|
119
|
+
const countName = `${stem}Count`;
|
|
120
|
+
const shownName = `shown${upperFirst(stem)}Count`;
|
|
121
|
+
const omittedName = `omitted${upperFirst(stem)}Count`;
|
|
122
|
+
const total = Math.max(numericValue(input[countName]), projection.totalCount);
|
|
123
|
+
output[countName] = total;
|
|
124
|
+
output[shownName] = projection.shownCount;
|
|
125
|
+
output[omittedName] = Math.max(0, total - projection.shownCount);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectionStem(key: string): string {
|
|
129
|
+
const stems: Record<string, string> = {
|
|
130
|
+
candidates: 'candidate',
|
|
131
|
+
candidateScores: 'candidateScore',
|
|
132
|
+
candidateFamilies: 'candidateFamily',
|
|
133
|
+
candidateEvidence: 'candidateEvidence',
|
|
134
|
+
candidatePaths: 'candidatePath',
|
|
135
|
+
candidateRawPaths: 'candidateRawPath',
|
|
136
|
+
candidateNormalizedOperationPaths: 'candidateNormalizedOperationPath',
|
|
137
|
+
normalizedCandidateOperations: 'normalizedCandidateOperation',
|
|
138
|
+
candidateLiterals: 'candidateLiteral',
|
|
139
|
+
bindingCandidates: 'bindingCandidate',
|
|
140
|
+
bindingAlternatives: 'bindingAlternative',
|
|
141
|
+
implementationHintSuggestions: 'implementationHintSuggestion',
|
|
142
|
+
selectableImplementationRepositories: 'selectableImplementationRepository',
|
|
143
|
+
matchedHints: 'matchedHint',
|
|
144
|
+
candidateSuggestions: 'candidateSuggestion',
|
|
145
|
+
dynamicTargetCandidates: 'dynamicTargetCandidate',
|
|
146
|
+
dynamicTargetCandidateSuggestions: 'dynamicTargetCandidateSuggestion',
|
|
147
|
+
rejectedCandidates: 'rejectedCandidate',
|
|
148
|
+
suggestedVarSets: 'suggestedVarSet',
|
|
149
|
+
copyableExamples: 'copyableExample',
|
|
150
|
+
selectorSuggestions: 'selectorSuggestion',
|
|
151
|
+
serviceSuggestions: 'serviceSuggestion',
|
|
152
|
+
repositories: 'repository',
|
|
153
|
+
examples: 'example',
|
|
154
|
+
expandedExamples: 'expandedExample',
|
|
155
|
+
registrations: 'registration',
|
|
156
|
+
};
|
|
157
|
+
return stems[key] ?? 'candidate';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function numericValue(value: unknown): number {
|
|
161
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function upperFirst(value: string): string {
|
|
165
|
+
return value ? `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}` : value;
|
|
166
|
+
}
|