@saptools/service-flow 0.1.44 → 0.1.45
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 +7 -0
- package/README.md +9 -1
- package/dist/{chunk-UKNPHTUS.js → chunk-BXSCE5CR.js} +1790 -1460
- package/dist/chunk-BXSCE5CR.js.map +1 -0
- package/dist/cli.js +73 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor.ts +49 -12
- package/src/cli.ts +10 -2
- package/src/index.ts +2 -0
- package/src/linker/cross-repo-linker.ts +23 -1
- package/src/linker/odata-path-normalizer.ts +4 -1
- package/src/parsers/imported-wrapper-parser.ts +262 -0
- package/src/parsers/outbound-call-parser.ts +5 -1
- package/src/parsers/symbol-parser.ts +25 -10
- package/src/trace/evidence.ts +6 -1
- package/src/trace/implementation-hints.ts +151 -0
- package/src/trace/trace-engine.ts +52 -45
- package/src/types.ts +8 -0
- package/dist/chunk-UKNPHTUS.js.map +0 -1
|
@@ -42,7 +42,8 @@ function isObjectFunction(node: ts.Node): boolean {
|
|
|
42
42
|
}
|
|
43
43
|
type ParameterBinding =
|
|
44
44
|
| { index: number; kind: 'identifier'; name: string }
|
|
45
|
-
| { index: number; kind: 'object_pattern'; properties: Array<{ property: string; local: string }> }
|
|
45
|
+
| { index: number; kind: 'object_pattern'; properties: Array<{ property: string; local: string }> }
|
|
46
|
+
| { index: number; kind: 'array_pattern'; elements: Array<{ index: number; local: string }> };
|
|
46
47
|
type ParameterPropertyAlias = { parameter: string; property: string; local: string; kind: 'object_parameter_destructure'; line: number };
|
|
47
48
|
const commonTerminalMembers = new Set(['push', 'includes', 'find', 'findIndex', 'map', 'filter', 'reduce', 'forEach', 'some', 'every', 'toUpperCase', 'toLowerCase', 'trim', 'split', 'join', 'get', 'set', 'has']);
|
|
48
49
|
const loggerMembers = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'log']);
|
|
@@ -99,6 +100,11 @@ function argumentEvidence(args: ts.NodeArray<ts.Expression>, source: ts.SourceFi
|
|
|
99
100
|
}
|
|
100
101
|
return { kind: 'object_literal', properties };
|
|
101
102
|
}
|
|
103
|
+
if (ts.isArrayLiteralExpression(arg)) {
|
|
104
|
+
const elements = arg.elements.flatMap((element, index): Array<Record<string, unknown>> =>
|
|
105
|
+
ts.isIdentifier(element) ? [{ index, kind: 'identifier', name: element.text }] : []);
|
|
106
|
+
return { kind: 'array_literal', elements };
|
|
107
|
+
}
|
|
102
108
|
return { kind: 'unsupported', expression: arg.getText(source) };
|
|
103
109
|
});
|
|
104
110
|
}
|
|
@@ -141,15 +147,24 @@ function parameterPropertyAliases(fn: ts.FunctionLikeDeclaration, source: ts.Sou
|
|
|
141
147
|
function parameterBindings(params: ts.NodeArray<ts.ParameterDeclaration>): ParameterBinding[] {
|
|
142
148
|
return params.flatMap((param, index): ParameterBinding[] => {
|
|
143
149
|
if (ts.isIdentifier(param.name)) return [{ index, kind: 'identifier', name: param.name.text }];
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
if (ts.isObjectBindingPattern(param.name)) {
|
|
151
|
+
const properties = param.name.elements.flatMap((element): Array<{ property: string; local: string }> => {
|
|
152
|
+
if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
|
|
153
|
+
const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
|
|
154
|
+
if (!property) return [];
|
|
155
|
+
const local = bindingLocalName(element.name, element.initializer);
|
|
156
|
+
return local ? [{ property, local }] : [];
|
|
157
|
+
});
|
|
158
|
+
return properties.length > 0 ? [{ index, kind: 'object_pattern', properties }] : [];
|
|
159
|
+
}
|
|
160
|
+
if (ts.isArrayBindingPattern(param.name)) {
|
|
161
|
+
const elements = param.name.elements.flatMap((element, elementIndex): Array<{ index: number; local: string }> =>
|
|
162
|
+
ts.isBindingElement(element) && !element.dotDotDotToken && ts.isIdentifier(element.name)
|
|
163
|
+
? [{ index: elementIndex, local: element.name.text }]
|
|
164
|
+
: []);
|
|
165
|
+
return elements.length > 0 ? [{ index, kind: 'array_pattern', elements }] : [];
|
|
166
|
+
}
|
|
167
|
+
return [];
|
|
153
168
|
});
|
|
154
169
|
}
|
|
155
170
|
export async function parseExecutableSymbols(repoPath: string, filePath: string): Promise<{ symbols: ExecutableSymbolFact[]; calls: SymbolCallFact[] }> {
|
package/src/trace/evidence.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
2
|
import { extractPlaceholders, substituteVariables, type RuntimeSubstitution } from '../linker/dynamic-edge-resolver.js';
|
|
3
|
+
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
3
4
|
import { resolveOperation, type OperationTarget } from '../linker/service-resolver.js';
|
|
4
5
|
|
|
5
6
|
export interface TraceGraphRow extends Record<string, unknown> {
|
|
@@ -154,7 +155,11 @@ function withEffectiveResolution(
|
|
|
154
155
|
|
|
155
156
|
function resolveRuntimeOperation(db: Db, evidence: Record<string, unknown>, workspaceId: number | undefined): ReturnType<typeof resolveOperation> {
|
|
156
157
|
const servicePath = stringValue(evidence.servicePath);
|
|
157
|
-
const
|
|
158
|
+
const rawOperationPath = stringValue(evidence.operationPath);
|
|
159
|
+
const normalized = normalizeODataOperationInvocationPath(rawOperationPath);
|
|
160
|
+
const operationPath = normalized?.wasInvocation
|
|
161
|
+
? normalized.normalizedOperationPath
|
|
162
|
+
: stringValue(evidence.normalizedOperationPath) ?? rawOperationPath;
|
|
158
163
|
const alias = stringValue(evidence.serviceAliasExpr ?? evidence.serviceAlias);
|
|
159
164
|
const destination = stringValue(evidence.destination);
|
|
160
165
|
return resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: true }, workspaceId);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ImplementationHint } from '../types.js';
|
|
2
|
+
|
|
3
|
+
interface Candidate {
|
|
4
|
+
accepted?: boolean;
|
|
5
|
+
methodId?: number;
|
|
6
|
+
sourceFile?: string;
|
|
7
|
+
handlerPackage?: { name?: string; packageName?: string };
|
|
8
|
+
modelPackage?: { name?: string; packageName?: string };
|
|
9
|
+
servicePath?: string;
|
|
10
|
+
operationPath?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface EdgeEvidence {
|
|
14
|
+
servicePath?: string;
|
|
15
|
+
operationPath?: string;
|
|
16
|
+
ambiguityReasons?: string[];
|
|
17
|
+
candidateFamilies?: Array<{ packageName?: string }>;
|
|
18
|
+
candidates?: Candidate[];
|
|
19
|
+
modelPackage?: { name?: string; packageName?: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ImplementationSelection {
|
|
23
|
+
methodId?: string;
|
|
24
|
+
blocksAutomatic: boolean;
|
|
25
|
+
evidence: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseImplementationHint(value: string): ImplementationHint {
|
|
29
|
+
const hint: Partial<ImplementationHint> = {};
|
|
30
|
+
for (const part of value.split(',')) {
|
|
31
|
+
const separator = part.indexOf('=');
|
|
32
|
+
if (separator <= 0 || separator === part.length - 1) throw new Error(`Invalid implementation hint field: ${part}`);
|
|
33
|
+
assignHintField(hint, part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
34
|
+
}
|
|
35
|
+
if (!hint.implementationRepo) throw new Error('Scoped implementation hint requires an implementation repo selection');
|
|
36
|
+
return { ...hint, implementationRepo: hint.implementationRepo };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function selectImplementation(
|
|
40
|
+
rawEvidence: Record<string, unknown>,
|
|
41
|
+
hints: ImplementationHint[] | undefined,
|
|
42
|
+
legacyRepo: string | undefined,
|
|
43
|
+
): ImplementationSelection {
|
|
44
|
+
const evidence = asEvidence(rawEvidence);
|
|
45
|
+
const scoped = hints ?? [];
|
|
46
|
+
const matchingHints = scoped.filter((hint) => hintMatchesEdge(hint, evidence));
|
|
47
|
+
if (matchingHints.length === 0) {
|
|
48
|
+
if (legacyRepo) return selectCandidate(evidence, legacyHint(legacyRepo), 'implementation_repo_hint');
|
|
49
|
+
const reason = scoped.length > 0 ? 'no_scoped_hint_matched_edge' : 'no_implementation_hint_supplied';
|
|
50
|
+
return { blocksAutomatic: false, evidence: { status: 'not_matched', reason, strategy: 'scoped_implementation_hint' } };
|
|
51
|
+
}
|
|
52
|
+
if (matchingHints.length > 1) {
|
|
53
|
+
return {
|
|
54
|
+
blocksAutomatic: true,
|
|
55
|
+
evidence: {
|
|
56
|
+
status: 'tied',
|
|
57
|
+
reason: 'multiple_scoped_hints_matched_edge',
|
|
58
|
+
strategy: 'scoped_implementation_hint',
|
|
59
|
+
matchedHints: matchingHints,
|
|
60
|
+
candidateCount: matchingHints.length,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const hint = matchingHints[0];
|
|
65
|
+
return hint ? selectCandidate(evidence, hint, 'scoped_implementation_hint') : { blocksAutomatic: false, evidence: { status: 'not_matched' } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function implementationHintDiagnostic(selection: ImplementationSelection): Record<string, unknown> | undefined {
|
|
69
|
+
if (!selection.blocksAutomatic || selection.methodId) return undefined;
|
|
70
|
+
return {
|
|
71
|
+
severity: 'warning',
|
|
72
|
+
code: 'implementation_hint_mismatch',
|
|
73
|
+
message: 'Implementation hint did not select exactly one viable candidate',
|
|
74
|
+
hintStatus: selection.evidence.status,
|
|
75
|
+
candidateCount: selection.evidence.candidateCount,
|
|
76
|
+
implementationSelection: selection.evidence,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assignHintField(hint: Partial<ImplementationHint>, key: string, value: string): void {
|
|
81
|
+
if (key === 'service' || key === 'servicePath') hint.servicePath = value;
|
|
82
|
+
else if (key === 'operation' || key === 'operationPath') hint.operationPath = value;
|
|
83
|
+
else if (key === 'package' || key === 'packageName') hint.packageName = value;
|
|
84
|
+
else if (key === 'repository' || key === 'repositoryName') hint.repositoryName = value;
|
|
85
|
+
else if (key === 'family' || key === 'candidateFamily') hint.candidateFamily = value;
|
|
86
|
+
else if (key === 'repo' || key === 'implementationRepo' || key === 'select') hint.implementationRepo = value;
|
|
87
|
+
else throw new Error(`Unknown implementation hint field: ${key}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function selectCandidate(evidence: EdgeEvidence, hint: ImplementationHint, strategy: string): ImplementationSelection {
|
|
91
|
+
const matches = (evidence.candidates ?? []).filter((candidate) =>
|
|
92
|
+
candidate.accepted && candidateMatchesRepo(candidate, hint.implementationRepo));
|
|
93
|
+
const selected = matches.length === 1 ? matches[0] : undefined;
|
|
94
|
+
if (!selected || selected.methodId === undefined) {
|
|
95
|
+
return {
|
|
96
|
+
blocksAutomatic: true,
|
|
97
|
+
evidence: {
|
|
98
|
+
status: matches.length > 1 ? 'tied' : 'not_matched',
|
|
99
|
+
reason: matches.length > 1 ? 'hint_matched_multiple_candidates' : 'hint_matched_zero_candidates',
|
|
100
|
+
strategy,
|
|
101
|
+
matchedHint: hint,
|
|
102
|
+
selectedRepo: hint.implementationRepo,
|
|
103
|
+
candidateCount: matches.length,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
methodId: String(selected.methodId),
|
|
109
|
+
blocksAutomatic: false,
|
|
110
|
+
evidence: {
|
|
111
|
+
status: 'selected',
|
|
112
|
+
guided: true,
|
|
113
|
+
strategy,
|
|
114
|
+
matchedHint: hint,
|
|
115
|
+
selectedRepo: hint.implementationRepo,
|
|
116
|
+
selectedMethodId: selected.methodId,
|
|
117
|
+
ambiguityReason: evidence.ambiguityReasons?.[0],
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hintMatchesEdge(hint: ImplementationHint, evidence: EdgeEvidence): boolean {
|
|
123
|
+
const model = evidence.modelPackage ?? evidence.candidates?.[0]?.modelPackage;
|
|
124
|
+
const familyNames = new Set([
|
|
125
|
+
...(evidence.candidateFamilies ?? []).flatMap((family) => family.packageName ? [family.packageName] : []),
|
|
126
|
+
...(evidence.candidates ?? []).flatMap((candidate) => candidate.handlerPackage?.packageName ? [candidate.handlerPackage.packageName] : []),
|
|
127
|
+
]);
|
|
128
|
+
return matches(hint.servicePath, evidence.servicePath ?? evidence.candidates?.[0]?.servicePath)
|
|
129
|
+
&& matches(hint.operationPath, evidence.operationPath ?? evidence.candidates?.[0]?.operationPath)
|
|
130
|
+
&& matches(hint.packageName, model?.packageName)
|
|
131
|
+
&& matches(hint.repositoryName, model?.name)
|
|
132
|
+
&& (!hint.candidateFamily || familyNames.has(hint.candidateFamily));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function candidateMatchesRepo(candidate: Candidate, value: string): boolean {
|
|
136
|
+
return candidate.handlerPackage?.name === value
|
|
137
|
+
|| candidate.handlerPackage?.packageName === value
|
|
138
|
+
|| candidate.sourceFile?.startsWith(value) === true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function matches(expected: string | undefined, actual: string | undefined): boolean {
|
|
142
|
+
return expected === undefined || expected === actual;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function legacyHint(implementationRepo: string): ImplementationHint {
|
|
146
|
+
return { implementationRepo };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function asEvidence(value: Record<string, unknown>): EdgeEvidence {
|
|
150
|
+
return value as EdgeEvidence;
|
|
151
|
+
}
|
|
@@ -2,8 +2,9 @@ import type { Db } from '../db/connection.js';
|
|
|
2
2
|
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
3
3
|
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
4
4
|
import { resolveOperation } from '../linker/service-resolver.js';
|
|
5
|
-
import type { TraceEdge, TraceResult, TraceStart } from '../types.js';
|
|
5
|
+
import type { ImplementationHint, TraceEdge, TraceResult, TraceStart } from '../types.js';
|
|
6
6
|
import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
7
|
+
import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
|
|
7
8
|
|
|
8
9
|
interface RepoRef {
|
|
9
10
|
id: number;
|
|
@@ -59,10 +60,12 @@ interface ContextBinding {
|
|
|
59
60
|
parameterPropertyAliasKind?: unknown;
|
|
60
61
|
parameterPropertyAliasLine?: unknown;
|
|
61
62
|
calleeReceiver: string;
|
|
63
|
+
callerSite?: { sourceFile?: string; sourceLine?: number };
|
|
64
|
+
calleeSite?: { sourceFile?: string; sourceLine?: number };
|
|
62
65
|
}
|
|
63
|
-
interface
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
interface ImplementationHintOptions {
|
|
67
|
+
implementationRepo?: string;
|
|
68
|
+
implementationHints?: ImplementationHint[];
|
|
66
69
|
}
|
|
67
70
|
function normalizeOperation(value: string | undefined): string | undefined {
|
|
68
71
|
if (!value) return undefined;
|
|
@@ -72,7 +75,7 @@ function positiveDepth(value: number): number {
|
|
|
72
75
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart,
|
|
78
|
+
function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, hintOptions: ImplementationHintOptions): { files?: Set<string>; symbols?: Set<number>; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
|
|
76
79
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
77
80
|
if (!requested) return undefined;
|
|
78
81
|
const rows = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,s.service_path servicePath,r.id repoId,r.name repoName
|
|
@@ -88,14 +91,16 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
|
|
|
88
91
|
const operationId = String(rows[0]?.operationId);
|
|
89
92
|
const impl = implementationScope(db, operationId);
|
|
90
93
|
if (impl.edge?.status === 'resolved' && impl.files.size > 0) return { files: impl.files, symbols: impl.symbolId ? new Set([impl.symbolId]) : undefined, operationId, diagnostics: [] };
|
|
91
|
-
const hinted = implementationMethodIdFromHint(impl.edge,
|
|
94
|
+
const hinted = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
92
95
|
if (hinted.methodId) {
|
|
93
96
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
94
97
|
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, operationId, diagnostics: [] };
|
|
95
98
|
}
|
|
96
99
|
if (impl.edge) {
|
|
97
100
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
98
|
-
|
|
101
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted);
|
|
102
|
+
const diagnostics = [{ severity: 'warning', code: impl.edge.status === 'ambiguous' ? 'trace_start_ambiguous' : 'trace_start_implementation_unresolved', message: `Indexed operation matched but implementation edge is ${String(impl.edge.status ?? 'unresolved')}`, resolutionStage: 'implementation', resolutionStatus: impl.edge.status === 'ambiguous' ? 'ambiguous_implementation' : 'rejected_implementation', implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, implementationAmbiguityReasons: evidence.ambiguityReasons, candidates: evidence.candidates }];
|
|
103
|
+
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
99
104
|
}
|
|
100
105
|
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' }] };
|
|
101
106
|
}
|
|
@@ -146,7 +151,7 @@ function sourceFilesForStart(
|
|
|
146
151
|
}
|
|
147
152
|
return undefined;
|
|
148
153
|
}
|
|
149
|
-
function startScope(db: Db, start: TraceStart,
|
|
154
|
+
function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions): StartScope {
|
|
150
155
|
const repo = start.repo
|
|
151
156
|
? (db
|
|
152
157
|
.prepare(
|
|
@@ -155,7 +160,7 @@ function startScope(db: Db, start: TraceStart, implementationRepo?: string): Sta
|
|
|
155
160
|
.get(start.repo, start.repo) as RepoRef | undefined)
|
|
156
161
|
: undefined;
|
|
157
162
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
158
|
-
const operationScope = operationStartScope(db, repo?.id, start,
|
|
163
|
+
const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
|
|
159
164
|
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === 'operation' || d.resolutionStage === 'implementation');
|
|
160
165
|
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
161
166
|
const sourceFiles = sourceScope?.files;
|
|
@@ -211,32 +216,16 @@ function implementationScope(db: Db, operationId: string): { repoId?: number; fi
|
|
|
211
216
|
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.name=hm.method_name WHERE hm.id=?').get(edge.to_id) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
212
217
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
213
218
|
}
|
|
214
|
-
function implementationMethodIdFromHint(edge: GraphRow | undefined,
|
|
215
|
-
if (!edge || edge.status !== 'ambiguous'
|
|
216
|
-
|
|
217
|
-
const matches = (evidence.candidates ?? []).filter((item) => item.accepted && (
|
|
218
|
-
item.handlerPackage?.name === implementationRepo ||
|
|
219
|
-
item.handlerPackage?.packageName === implementationRepo ||
|
|
220
|
-
item.sourceFile?.startsWith(implementationRepo)
|
|
221
|
-
));
|
|
222
|
-
if (matches.length !== 1 || matches[0]?.methodId === undefined) return { evidence: { status: matches.length > 1 ? 'tied' : 'not_matched', strategy: 'implementation_repo_hint', selectedRepo: implementationRepo, candidateCount: matches.length } };
|
|
223
|
-
return {
|
|
224
|
-
methodId: String(matches[0].methodId),
|
|
225
|
-
evidence: {
|
|
226
|
-
status: 'selected',
|
|
227
|
-
guided: true,
|
|
228
|
-
strategy: 'implementation_repo_hint',
|
|
229
|
-
selectedRepo: implementationRepo,
|
|
230
|
-
selectedMethodId: matches[0].methodId,
|
|
231
|
-
ambiguityReason: evidence.ambiguityReasons?.[0],
|
|
232
|
-
},
|
|
233
|
-
};
|
|
219
|
+
function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
|
|
220
|
+
if (!edge || edge.status !== 'ambiguous') return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
|
|
221
|
+
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
234
222
|
}
|
|
235
223
|
|
|
236
|
-
function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown
|
|
237
|
-
const hinted = implementationMethodIdFromHint(edge,
|
|
224
|
+
function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
|
|
225
|
+
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
238
226
|
if (hinted.methodId) return hinted;
|
|
239
|
-
if (
|
|
227
|
+
if (hinted.blocksAutomatic) return hinted;
|
|
228
|
+
if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
|
|
240
229
|
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 }> };
|
|
241
230
|
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
242
231
|
const reasons: string[] = [];
|
|
@@ -246,10 +235,10 @@ function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId:
|
|
|
246
235
|
if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
|
|
247
236
|
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
248
237
|
}).sort((a, b) => b.score - a.score);
|
|
249
|
-
if (scores.length === 0) return { evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
238
|
+
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
250
239
|
const [first, second] = scores;
|
|
251
|
-
if (first && first.methodId !== undefined && first.score > 0 && (!second || first.score > second.score)) return { methodId: String(first.methodId), evidence: { status: 'selected', selectedMethodId: first.methodId, candidateScores: scores } };
|
|
252
|
-
return { evidence: { status: 'tied', tieReason: scores.length > 1 ? 'duplicate_helper_implementation_candidates' : 'no_unique_materially_stronger_candidate', candidateScores: scores } };
|
|
240
|
+
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 } };
|
|
241
|
+
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 } };
|
|
253
242
|
}
|
|
254
243
|
function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
|
|
255
244
|
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.name=hm.method_name WHERE hm.id=?').get(methodId) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
@@ -361,18 +350,22 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
361
350
|
const next = new Map<string, ContextBinding>();
|
|
362
351
|
if (callerBindings.size === 0) return next;
|
|
363
352
|
const callEvidence = parseEvidence(symbolCall.evidence_json);
|
|
364
|
-
const callee = db.prepare('SELECT evidence_json evidenceJson FROM symbols WHERE id=?').get(symbolCall.callee_symbol_id) as { evidenceJson?: string } | undefined;
|
|
353
|
+
const callee = db.prepare('SELECT evidence_json evidenceJson,source_file sourceFile,start_line startLine FROM symbols WHERE id=?').get(symbolCall.callee_symbol_id) as { evidenceJson?: string; sourceFile?: string; startLine?: number } | undefined;
|
|
365
354
|
const calleeEvidence = parseEvidence(callee?.evidenceJson);
|
|
366
355
|
const params = Array.isArray(calleeEvidence.parameters) ? calleeEvidence.parameters.filter((item): item is string => typeof item === 'string') : [];
|
|
367
356
|
const parameterBindings = Array.isArray(calleeEvidence.parameterBindings) ? calleeEvidence.parameterBindings.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
368
357
|
const parameterPropertyAliases = Array.isArray(calleeEvidence.parameterPropertyAliases) ? calleeEvidence.parameterPropertyAliases.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object' && !Array.isArray(item))) : [];
|
|
369
358
|
const args = Array.isArray(callEvidence.callArguments) ? callEvidence.callArguments as Array<Record<string, unknown>> : [];
|
|
359
|
+
const provenance = {
|
|
360
|
+
callerSite: { sourceFile: String(symbolCall.source_file ?? ''), sourceLine: Number(symbolCall.source_line ?? 0) },
|
|
361
|
+
calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine },
|
|
362
|
+
};
|
|
370
363
|
args.forEach((arg, index) => {
|
|
371
364
|
const paramBinding = parameterBindings.find((binding) => binding.index === index);
|
|
372
365
|
const param = paramBinding?.kind === 'identifier' && typeof paramBinding.name === 'string' ? paramBinding.name : params[index];
|
|
373
366
|
if (arg.kind === 'identifier' && typeof arg.name === 'string') {
|
|
374
367
|
const binding = callerBindings.get(arg.name);
|
|
375
|
-
if (binding && param) next.set(param, { ...binding, source: 'local_symbol_argument', callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
368
|
+
if (binding && param) next.set(param, { ...binding, ...provenance, source: 'local_symbol_argument', callerArgument: arg.name, calleeParameter: param, calleeReceiver: param });
|
|
376
369
|
}
|
|
377
370
|
if (arg.kind === 'object_literal' && Array.isArray(arg.properties)) {
|
|
378
371
|
for (const prop of arg.properties as Array<Record<string, unknown>>) {
|
|
@@ -382,15 +375,23 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
382
375
|
const destructured = paramBinding?.kind === 'object_pattern' && Array.isArray(paramBinding.properties)
|
|
383
376
|
? (paramBinding.properties as Array<Record<string, unknown>>).find((item) => item.property === prop.property && typeof item.local === 'string')
|
|
384
377
|
: undefined;
|
|
385
|
-
if (destructured && typeof destructured.local === 'string') next.set(destructured.local, { ...binding, source: 'local_symbol_destructured_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
|
|
378
|
+
if (destructured && typeof destructured.local === 'string') next.set(destructured.local, { ...binding, ...provenance, source: 'local_symbol_destructured_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: String(index), calleeReceiver: destructured.local });
|
|
386
379
|
else if (param) {
|
|
387
|
-
next.set(`${param}.${prop.property}`, { ...binding, source: 'local_symbol_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
380
|
+
next.set(`${param}.${prop.property}`, { ...binding, ...provenance, source: 'local_symbol_object_argument', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeReceiver: `${param}.${prop.property}` });
|
|
388
381
|
for (const alias of parameterPropertyAliases) {
|
|
389
|
-
if (alias.parameter === param && alias.property === prop.property && typeof alias.local === 'string') next.set(alias.local, { ...binding, source: 'local_symbol_object_parameter_destructure', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeObjectProperty: `${param}.${prop.property}`, calleeReceiver: alias.local, calleeLocalDestructuredIdentifier: alias.local, parameterPropertyAliasKind: alias.kind, parameterPropertyAliasLine: alias.line });
|
|
382
|
+
if (alias.parameter === param && alias.property === prop.property && typeof alias.local === 'string') next.set(alias.local, { ...binding, ...provenance, source: 'local_symbol_object_parameter_destructure', callerProperty: prop.property, callerArgument: prop.argument, calleeParameter: param, calleeObjectProperty: `${param}.${prop.property}`, calleeReceiver: alias.local, calleeLocalDestructuredIdentifier: alias.local, parameterPropertyAliasKind: alias.kind, parameterPropertyAliasLine: alias.line });
|
|
390
383
|
}
|
|
391
384
|
}
|
|
392
385
|
}
|
|
393
386
|
}
|
|
387
|
+
if (arg.kind === 'array_literal' && Array.isArray(arg.elements) && paramBinding?.kind === 'array_pattern' && Array.isArray(paramBinding.elements)) {
|
|
388
|
+
for (const element of arg.elements as Array<Record<string, unknown>>) {
|
|
389
|
+
const target = (paramBinding.elements as Array<Record<string, unknown>>).find((item) => item.index === element.index);
|
|
390
|
+
if (element.kind !== 'identifier' || typeof element.name !== 'string' || typeof target?.local !== 'string') continue;
|
|
391
|
+
const binding = callerBindings.get(element.name);
|
|
392
|
+
if (binding) next.set(target.local, { ...binding, ...provenance, source: 'local_symbol_destructured_array_argument', callerArgument: element.name, calleeParameter: String(index), calleeReceiver: target.local });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
394
395
|
});
|
|
395
396
|
return next;
|
|
396
397
|
}
|
|
@@ -401,7 +402,7 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
|
|
|
401
402
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
402
403
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
403
404
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
404
|
-
const evidence: Record<string, unknown> = { contextualServiceBindingAttempted: true, contextualBinding: { source: binding.source, callerArgument: binding.callerArgument, callerProperty: binding.callerProperty, calleeParameter: binding.calleeParameter, calleeReceiver: binding.calleeReceiver, 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 };
|
|
405
|
+
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 };
|
|
405
406
|
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' };
|
|
406
407
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
407
408
|
const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
|
|
@@ -418,9 +419,11 @@ export function trace(
|
|
|
418
419
|
includeDb?: boolean;
|
|
419
420
|
includeAsync?: boolean;
|
|
420
421
|
implementationRepo?: string;
|
|
422
|
+
implementationHints?: ImplementationHint[];
|
|
421
423
|
},
|
|
422
424
|
): TraceResult {
|
|
423
|
-
const
|
|
425
|
+
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
426
|
+
const scope = startScope(db, start, hintOptions);
|
|
424
427
|
const diagnostics = db
|
|
425
428
|
.prepare(
|
|
426
429
|
'SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics WHERE (? IS NULL OR repo_id=?)',
|
|
@@ -448,7 +451,7 @@ export function trace(
|
|
|
448
451
|
const op = operationNode(db, scope.startOperationId);
|
|
449
452
|
const impl = implementationScope(db, scope.startOperationId);
|
|
450
453
|
if (op) nodes.set(String(op.id), op);
|
|
451
|
-
const startSelection = implementationMethodIdFromHint(impl.edge,
|
|
454
|
+
const startSelection = implementationMethodIdFromHint(impl.edge, hintOptions);
|
|
452
455
|
if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
|
|
453
456
|
const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
|
|
454
457
|
const implEvidence = { ...parseEvidence(impl.edge.evidence_json), startResolution: { strategy: 'indexed_operation_graph', matchedOperationId: scope.startOperationId, implementationEdgeId: impl.edge.id, implementationStatus: impl.edge.status, selectedHandlerMethodId: selectedMethodId }, implementationSelection: startSelection.methodId ? startSelection.evidence : undefined };
|
|
@@ -540,10 +543,12 @@ export function trace(
|
|
|
540
543
|
});
|
|
541
544
|
if (effectiveRow.to_kind === 'operation') {
|
|
542
545
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
543
|
-
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence,
|
|
546
|
+
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
|
|
544
547
|
const contextMethodId = contextSelection.methodId;
|
|
545
548
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
|
|
546
549
|
if (implementation.edge) {
|
|
550
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection);
|
|
551
|
+
if (hintDiagnostic) diagnostics.unshift(hintDiagnostic);
|
|
547
552
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
|
|
548
553
|
const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
549
554
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
@@ -553,7 +558,9 @@ export function trace(
|
|
|
553
558
|
type: 'operation_implemented_by_handler',
|
|
554
559
|
from: to,
|
|
555
560
|
to: implTo,
|
|
556
|
-
evidence: contextMethodId
|
|
561
|
+
evidence: contextMethodId
|
|
562
|
+
? { ...implEvidence, contextualImplementationSelected: contextSelection.evidence.strategy !== 'implementation_repo_hint', contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence }
|
|
563
|
+
: { ...implEvidence, contextualImplementation: contextSelection.evidence, implementationSelection: contextSelection.evidence },
|
|
557
564
|
confidence: Number(implementation.edge.confidence ?? 0),
|
|
558
565
|
unresolvedReason: implementation.edge.status === 'resolved' || contextMethodId ? undefined : String(implementation.edge.unresolved_reason ?? implementation.edge.status),
|
|
559
566
|
});
|
package/src/types.ts
CHANGED
|
@@ -179,6 +179,14 @@ export interface TraceStart {
|
|
|
179
179
|
operationPath?: string;
|
|
180
180
|
handler?: string;
|
|
181
181
|
}
|
|
182
|
+
export interface ImplementationHint {
|
|
183
|
+
servicePath?: string;
|
|
184
|
+
operationPath?: string;
|
|
185
|
+
packageName?: string;
|
|
186
|
+
repositoryName?: string;
|
|
187
|
+
candidateFamily?: string;
|
|
188
|
+
implementationRepo: string;
|
|
189
|
+
}
|
|
182
190
|
export interface TraceEdge {
|
|
183
191
|
step: number;
|
|
184
192
|
type: string;
|