@saptools/service-flow 0.1.51 → 0.1.53
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 +13 -0
- package/README.md +16 -14
- package/TECHNICAL-NOTE.md +18 -6
- package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
- package/dist/chunk-LFH7C46B.js.map +1 -0
- package/dist/cli.js +435 -515
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli/001-doctor-projection.ts +140 -0
- package/src/cli/doctor.ts +20 -3
- package/src/cli.ts +75 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +130 -24
- package/src/db/schema.ts +7 -2
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/000-implementation-candidates.ts +641 -0
- package/src/linker/001-implementation-evidence-projection.ts +119 -0
- package/src/linker/002-call-evidence.ts +226 -0
- package/src/linker/cross-repo-linker.ts +27 -441
- 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 +33 -5
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +96 -0
- package/src/trace/001-dynamic-identity.ts +308 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +283 -0
- 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/dynamic-targets.ts +582 -306
- package/src/trace/evidence.ts +331 -46
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +551 -3
- package/src/trace/trace-engine.ts +129 -135
- package/src/types.ts +27 -1
- package/src/utils/000-bounded-projection.ts +161 -0
- package/dist/chunk-YZJKE5UX.js.map +0 -1
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { reposByName } from '../db/repositories.js';
|
|
2
3
|
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
3
4
|
import { normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
4
5
|
import { resolveOperation } from '../linker/service-resolver.js';
|
|
5
6
|
import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
|
|
6
|
-
import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
7
|
+
import { baseTraceEvidence, edgeTarget, runtimeNoCandidateDiagnostics, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
7
8
|
import { dynamicCandidateBranches } from './dynamic-branches.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
loadTraceDiagnostics,
|
|
11
|
+
prependTraceDiagnostic,
|
|
12
|
+
} from './002-trace-diagnostics.js';
|
|
13
|
+
import { implementationHintDiagnostic } from './implementation-hints.js';
|
|
14
|
+
import {
|
|
15
|
+
contextualImplementationSelection,
|
|
16
|
+
hintedImplementationSelection,
|
|
17
|
+
} from './005-implementation-selection.js';
|
|
18
|
+
import { boundedContextCandidates } from './006-contextual-projection.js';
|
|
19
|
+
import { implementationStartDiagnostic } from './007-implementation-start-diagnostic.js';
|
|
20
|
+
import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
|
|
21
|
+
import {
|
|
22
|
+
ambiguousStartDiagnostic,
|
|
23
|
+
selectorNotFoundDiagnostic,
|
|
24
|
+
selectorRepoAmbiguousDiagnostic,
|
|
25
|
+
selectorRepoNotFoundDiagnostic,
|
|
26
|
+
sourceScopeForSelector,
|
|
27
|
+
} from './selectors.js';
|
|
10
28
|
interface RepoRef {
|
|
11
29
|
id: number;
|
|
12
30
|
name: string;
|
|
31
|
+
packageName?: string;
|
|
13
32
|
}
|
|
14
33
|
interface StartScope {
|
|
15
34
|
repo?: RepoRef;
|
|
@@ -78,13 +97,14 @@ function normalizeOperation(value: string | undefined): string | undefined {
|
|
|
78
97
|
function positiveDepth(value: number): number {
|
|
79
98
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
80
99
|
}
|
|
81
|
-
function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, hintOptions: ImplementationHintOptions): { files?: Set<string>; symbols?: Set<number>; repoId?: number; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
|
|
100
|
+
function operationStartScope(db: Db, repoId: number | undefined, start: TraceStart, hintOptions: ImplementationHintOptions, workspaceId?: number): { files?: Set<string>; symbols?: Set<number>; repoId?: number; operationId?: string; diagnostics?: Array<Record<string, unknown>> } | undefined {
|
|
82
101
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
83
102
|
if (!requested) return undefined;
|
|
84
103
|
const rows = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.service_path servicePath,r.id repoId,r.name repoName
|
|
85
104
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
86
|
-
WHERE (? IS NULL OR r.
|
|
87
|
-
|
|
105
|
+
WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR r.id=?)
|
|
106
|
+
AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
|
|
107
|
+
ORDER BY r.name,s.service_path,o.operation_name,o.id`).all(workspaceId, workspaceId, repoId, repoId, start.servicePath, start.servicePath, requested, requested, requested.startsWith('/') ? requested : `/${requested}`) as Array<Record<string, unknown>>;
|
|
88
108
|
if (rows.length === 0) return undefined;
|
|
89
109
|
const repoCount = new Set(rows.map((row) => String(row.repoName))).size;
|
|
90
110
|
const serviceCount = new Set(rows.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
|
|
@@ -97,89 +117,52 @@ function operationStartScope(db: Db, repoId: number | undefined, start: TraceSta
|
|
|
97
117
|
const operationId = String(rows[0]?.operationId);
|
|
98
118
|
const impl = implementationScope(db, operationId);
|
|
99
119
|
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: [] };
|
|
100
|
-
const hinted =
|
|
120
|
+
const hinted = hintedImplementationSelection(
|
|
121
|
+
db, impl.edge, operationId, hintOptions,
|
|
122
|
+
);
|
|
101
123
|
if (hinted.methodId) {
|
|
102
124
|
const hintedScope = handlerScope(db, hinted.methodId);
|
|
103
125
|
if (hintedScope?.files.size) return { files: hintedScope.files, symbols: hintedScope.symbolId ? new Set([hintedScope.symbolId]) : undefined, repoId: hintedScope.repoId, operationId, diagnostics: [] };
|
|
104
126
|
}
|
|
105
127
|
if (impl.edge) {
|
|
106
128
|
const evidence = parseEvidence(impl.edge.evidence_json);
|
|
107
|
-
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence
|
|
108
|
-
const diagnostics = [
|
|
129
|
+
const hintDiagnostic = implementationHintDiagnostic(hinted, evidence);
|
|
130
|
+
const diagnostics = [implementationStartDiagnostic(impl.edge, evidence)];
|
|
109
131
|
return { operationId, diagnostics: hintDiagnostic ? [hintDiagnostic, ...diagnostics] : diagnostics };
|
|
110
132
|
}
|
|
111
133
|
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' }] };
|
|
112
134
|
}
|
|
113
|
-
function implementationRejectionReasons(evidence: Record<string, unknown>): string[] {
|
|
114
|
-
const candidates = Array.isArray(evidence.candidates)
|
|
115
|
-
? evidence.candidates.filter((item): item is Record<string, unknown> =>
|
|
116
|
-
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
117
|
-
: [];
|
|
118
|
-
const reasons = candidates.flatMap((candidate) =>
|
|
119
|
-
Array.isArray(candidate.rejectedReasons)
|
|
120
|
-
? candidate.rejectedReasons.filter((reason): reason is string =>
|
|
121
|
-
typeof reason === 'string')
|
|
122
|
-
: []);
|
|
123
|
-
return [...new Set(reasons)].sort();
|
|
124
|
-
}
|
|
125
135
|
function sourceFilesForStart(
|
|
126
136
|
db: Db,
|
|
127
137
|
repoId: number | undefined,
|
|
128
138
|
start: TraceStart,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
operation,
|
|
154
|
-
operation,
|
|
155
|
-
operation,
|
|
156
|
-
operation,
|
|
157
|
-
) as Array<{ sourceFile?: string; symbolId?: number }>;
|
|
158
|
-
if (rows.length > 0) return { files: new Set(rows.map((row) => row.sourceFile).filter(Boolean) as string[]), symbols: new Set(rows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
|
|
159
|
-
if (start.servicePath && operation) {
|
|
160
|
-
const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
|
|
161
|
-
FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
|
|
162
|
-
JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved' AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
|
|
163
|
-
JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
|
|
164
|
-
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
165
|
-
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
|
|
166
|
-
WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation) as Array<{ sourceFile?: string; symbolId?: number }>;
|
|
167
|
-
if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean) as string[]), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)), repoId };
|
|
168
|
-
}
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions): StartScope {
|
|
172
|
-
const repo = start.repo
|
|
173
|
-
? (db
|
|
174
|
-
.prepare(
|
|
175
|
-
'SELECT id,name FROM repositories WHERE name=? OR package_name=?',
|
|
176
|
-
)
|
|
177
|
-
.get(start.repo, start.repo) as RepoRef | undefined)
|
|
178
|
-
: undefined;
|
|
179
|
-
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
180
|
-
const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
|
|
139
|
+
workspaceId: number | undefined,
|
|
140
|
+
): ReturnType<typeof sourceScopeForSelector> {
|
|
141
|
+
return sourceScopeForSelector(db, repoId, start, workspaceId);
|
|
142
|
+
}
|
|
143
|
+
function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions, workspaceId?: number): StartScope {
|
|
144
|
+
const repos: RepoRef[] = start.repo
|
|
145
|
+
? reposByName(db, start.repo, workspaceId).map((row) => ({
|
|
146
|
+
id: row.id,
|
|
147
|
+
name: row.name,
|
|
148
|
+
packageName: row.package_name ?? undefined,
|
|
149
|
+
}))
|
|
150
|
+
: [];
|
|
151
|
+
if (start.repo && repos.length === 0) return {
|
|
152
|
+
selectorMatched: false,
|
|
153
|
+
startDiagnostics: [selectorRepoNotFoundDiagnostic(start.repo)],
|
|
154
|
+
};
|
|
155
|
+
if (start.repo && repos.length > 1) return {
|
|
156
|
+
selectorMatched: false,
|
|
157
|
+
startDiagnostics: [selectorRepoAmbiguousDiagnostic(start.repo, repos)],
|
|
158
|
+
};
|
|
159
|
+
const repo = repos[0];
|
|
160
|
+
const operationScope = operationStartScope(
|
|
161
|
+
db, repo?.id, start, hintOptions, workspaceId,
|
|
162
|
+
);
|
|
181
163
|
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === 'operation' || d.resolutionStage === 'implementation');
|
|
182
|
-
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
164
|
+
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start, workspaceId);
|
|
165
|
+
const terminalSelectorScope = Boolean(sourceScope?.diagnostics?.length && !sourceScope.files);
|
|
183
166
|
const sourceFiles = sourceScope?.files;
|
|
184
167
|
const hasSelector = Boolean(
|
|
185
168
|
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath,
|
|
@@ -191,9 +174,12 @@ function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOp
|
|
|
191
174
|
executionRepoId: sourceScope?.repoId ?? repo?.id,
|
|
192
175
|
sourceFiles,
|
|
193
176
|
symbolIds: sourceScope?.symbols,
|
|
194
|
-
selectorMatched: !terminalOperationScope &&
|
|
177
|
+
selectorMatched: !terminalOperationScope && !terminalSelectorScope
|
|
178
|
+
&& (!hasSelector || sourceFiles !== undefined),
|
|
195
179
|
startOperationId: operationScope?.operationId,
|
|
196
|
-
startDiagnostics: operationScope?.diagnostics
|
|
180
|
+
startDiagnostics: operationScope?.diagnostics?.length
|
|
181
|
+
? operationScope.diagnostics
|
|
182
|
+
: sourceScope?.diagnostics,
|
|
197
183
|
};
|
|
198
184
|
}
|
|
199
185
|
function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
|
|
@@ -211,8 +197,19 @@ function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
|
|
|
211
197
|
.prepare(
|
|
212
198
|
`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
|
|
213
199
|
JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
214
|
-
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
|
|
215
|
-
|
|
200
|
+
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
|
|
201
|
+
AND sym.source_file=hc.source_file
|
|
202
|
+
AND sym.qualified_name=hc.class_name || '.' || hm.method_name
|
|
203
|
+
AND sym.start_line=hm.source_line
|
|
204
|
+
WHERE hc.repo_id=?
|
|
205
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
|
|
206
|
+
CASE WHEN hm.decorator_kind='Event' THEN 'event'
|
|
207
|
+
WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
|
|
208
|
+
ELSE 'unsupported' END)='operation'
|
|
209
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
|
|
210
|
+
CASE WHEN hm.decorator_kind IN ('Action','Func','On')
|
|
211
|
+
THEN 1 ELSE 0 END)=1
|
|
212
|
+
AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`,
|
|
216
213
|
)
|
|
217
214
|
.all(op.repoId, operation, operation, op.operationName) as Array<{
|
|
218
215
|
sourceFile?: string;
|
|
@@ -230,35 +227,14 @@ function handlerMethodNode(db: Db, methodId: string): Record<string, unknown> |
|
|
|
230
227
|
function implementationScope(db: Db, operationId: string): { repoId?: number; files: Set<string>; symbolId?: number; edge?: GraphRow } {
|
|
231
228
|
const edge = implementationEdge(db, operationId);
|
|
232
229
|
if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
|
|
233
|
-
const row = db.prepare(
|
|
230
|
+
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(edge.to_id) as { repoId?: number; sourceFile?: string; symbolId?: number } | undefined;
|
|
231
|
+
if (!row || typeof row.symbolId !== 'number')
|
|
232
|
+
return { repoId: row?.repoId, files: new Set(), edge };
|
|
234
233
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
235
234
|
}
|
|
236
|
-
function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
|
|
237
|
-
if (!edge || edge.status !== 'ambiguous') return { blocksAutomatic: false, evidence: { status: 'not_applicable' } };
|
|
238
|
-
return selectImplementation(parseEvidence(edge.evidence_json), options.implementationHints, options.implementationRepo);
|
|
239
|
-
}
|
|
240
|
-
function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId: number | undefined, remoteEvidence: Record<string, unknown>, hintOptions: ImplementationHintOptions): ImplementationSelection {
|
|
241
|
-
const hinted = implementationMethodIdFromHint(edge, hintOptions);
|
|
242
|
-
if (hinted.methodId) return hinted;
|
|
243
|
-
if (hinted.blocksAutomatic) return hinted;
|
|
244
|
-
if (!edge || edge.status !== 'ambiguous' || callerRepoId === undefined) return hinted;
|
|
245
|
-
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 }> };
|
|
246
|
-
const scores = (evidence.candidates ?? []).filter((item) => item.accepted).map((item) => {
|
|
247
|
-
const reasons: string[] = [];
|
|
248
|
-
let score = Number(item.score ?? 0);
|
|
249
|
-
if (Number(item.handlerPackage?.id) === callerRepoId) { score += 10; reasons.push('handler_package_matches_caller_repository'); }
|
|
250
|
-
if (Number(item.applicationPackage?.id) === callerRepoId) { score += 10; reasons.push('registration_package_matches_caller_repository'); }
|
|
251
|
-
if (typeof remoteEvidence.effectiveServicePath === 'string' || typeof remoteEvidence.effectiveDestination === 'string' || typeof remoteEvidence.effectiveAlias === 'string') { score += 1; reasons.push('remote_call_context_available'); }
|
|
252
|
-
return { methodId: item.methodId, score, reasons, handlerPackage: item.handlerPackage, applicationPackage: item.applicationPackage };
|
|
253
|
-
}).sort((a, b) => b.score - a.score);
|
|
254
|
-
if (scores.length === 0) return { blocksAutomatic: false, evidence: { status: 'not_applicable', candidateScores: [] } };
|
|
255
|
-
const [first, second] = scores;
|
|
256
|
-
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 } };
|
|
257
|
-
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 } };
|
|
258
|
-
}
|
|
259
235
|
function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
|
|
260
|
-
const row = db.prepare(
|
|
261
|
-
if (!row) return undefined;
|
|
236
|
+
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;
|
|
237
|
+
if (!row || typeof row.symbolId !== 'number') return undefined;
|
|
262
238
|
return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
|
|
263
239
|
}
|
|
264
240
|
|
|
@@ -315,11 +291,15 @@ function workspaceIdForCall(db: Db, callId: string): number | undefined {
|
|
|
315
291
|
function parseEvidence(value: unknown): Record<string, unknown> {
|
|
316
292
|
try {
|
|
317
293
|
const parsed = JSON.parse(String(value || '{}')) as unknown;
|
|
318
|
-
return
|
|
294
|
+
return isEvidenceRecord(parsed) ? boundCandidateLikeEvidence(parsed) : {};
|
|
319
295
|
} catch {
|
|
320
296
|
return {};
|
|
321
297
|
}
|
|
322
298
|
}
|
|
299
|
+
|
|
300
|
+
function isEvidenceRecord(value: unknown): value is Record<string, unknown> {
|
|
301
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
302
|
+
}
|
|
323
303
|
function receiverFromEvidence(value: unknown): string | undefined {
|
|
324
304
|
const evidence = parseEvidence(value);
|
|
325
305
|
return typeof evidence.receiver === 'string' ? evidence.receiver : undefined;
|
|
@@ -459,16 +439,20 @@ function contextForSymbolCall(db: Db, symbolCall: Record<string, unknown>, calle
|
|
|
459
439
|
function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBinding | undefined, workspaceId: number | undefined, persistedRows: GraphRow[] = []): { row?: GraphRow; evidence?: Record<string, unknown>; unresolvedReason?: string } {
|
|
460
440
|
if (!binding || String(call.call_type) !== 'remote_action' || call.operation_path_expr === undefined || call.operation_path_expr === null) return {};
|
|
461
441
|
if (binding.resolutionStatus === 'ambiguous') {
|
|
442
|
+
const candidates = boundedContextCandidates(binding.bindingCandidates ?? []);
|
|
462
443
|
return {
|
|
463
444
|
evidence: {
|
|
464
445
|
contextualServiceBindingAttempted: true,
|
|
465
446
|
contextualBinding: {
|
|
466
447
|
source: binding.source,
|
|
467
448
|
status: 'tied',
|
|
468
|
-
candidates:
|
|
449
|
+
candidates: candidates.candidates,
|
|
450
|
+
candidateCount: candidates.candidateCount,
|
|
451
|
+
shownCandidateCount: candidates.shownCandidateCount,
|
|
452
|
+
omittedCandidateCount: candidates.omittedCandidateCount,
|
|
469
453
|
},
|
|
470
454
|
contextualResolutionStatus: 'ambiguous',
|
|
471
|
-
contextualCandidateCount:
|
|
455
|
+
contextualCandidateCount: candidates.candidateCount,
|
|
472
456
|
},
|
|
473
457
|
unresolvedReason: 'Ambiguous contextual service binding candidates',
|
|
474
458
|
};
|
|
@@ -478,7 +462,8 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
|
|
|
478
462
|
const servicePath = binding.effectiveServicePath ?? binding.servicePathExpr ?? binding.requireServicePath;
|
|
479
463
|
const destination = binding.effectiveDestination ?? binding.destinationExpr ?? binding.requireDestination;
|
|
480
464
|
const resolution = resolveOperation(db, { servicePath, operationPath: op, alias: binding.aliasExpr ?? binding.alias, destination, hasExplicitOverride: true, isDynamic: false }, workspaceId);
|
|
481
|
-
const
|
|
465
|
+
const candidates = boundedContextCandidates(resolution.candidates);
|
|
466
|
+
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: candidates.candidateCount, shownContextualCandidateCount: candidates.shownCandidateCount, omittedContextualCandidateCount: candidates.omittedCandidateCount, candidates: candidates.candidates, contextualResolutionReasons: resolution.reasons, resolutionReasons: resolution.reasons };
|
|
482
467
|
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' };
|
|
483
468
|
const resolvedEvidence = { ...evidence, contextualServiceBindingSelected: true, targetRepo: resolution.target.repoName, targetServicePath: resolution.target.servicePath, targetOperationPath: resolution.target.operationPath, targetOperation: resolution.target.operationName };
|
|
484
469
|
const persistedResolved = persistedRows.find((item) => item.status === 'resolved');
|
|
@@ -491,22 +476,25 @@ export function trace(
|
|
|
491
476
|
options: TraceOptions,
|
|
492
477
|
): TraceResult {
|
|
493
478
|
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
494
|
-
const scope = startScope(db, start, hintOptions);
|
|
495
|
-
const
|
|
496
|
-
.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
479
|
+
const scope = startScope(db, start, hintOptions, options.workspaceId);
|
|
480
|
+
const hasSelector = Boolean(start.repo || start.handler || start.operation
|
|
481
|
+
|| start.operationPath || start.servicePath);
|
|
482
|
+
const diagnosticRepoId = scope.executionRepoId ?? scope.repo?.id;
|
|
483
|
+
const diagnostics = loadTraceDiagnostics(
|
|
484
|
+
db,
|
|
485
|
+
diagnosticRepoId,
|
|
486
|
+
!hasSelector,
|
|
487
|
+
options.workspaceId,
|
|
488
|
+
);
|
|
489
|
+
const stale = diagnosticRepoId !== undefined || !hasSelector
|
|
490
|
+
? db.prepare('SELECT name,graph_stale_reason reason FROM repositories WHERE graph_stale_reason IS NOT NULL AND (? IS NULL OR id=?) AND (? IS NULL OR workspace_id=?) ORDER BY name,id').all(diagnosticRepoId, diagnosticRepoId, options.workspaceId, options.workspaceId) as Array<{ name?: string; reason?: string }>
|
|
491
|
+
: [];
|
|
501
492
|
for (const row of stale)
|
|
502
|
-
diagnostics
|
|
503
|
-
for (const diagnostic of scope.startDiagnostics ?? [])
|
|
493
|
+
prependTraceDiagnostic(diagnostics, { severity: 'warning', code: 'graph_stale', message: `Graph is stale for ${row.name ?? 'repository'}: ${row.reason ?? 'facts_changed'}. Run service-flow link.` });
|
|
494
|
+
for (const diagnostic of scope.startDiagnostics ?? [])
|
|
495
|
+
prependTraceDiagnostic(diagnostics, diagnostic);
|
|
504
496
|
if (!scope.selectorMatched && !(scope.startDiagnostics?.length))
|
|
505
|
-
diagnostics
|
|
506
|
-
severity: 'warning',
|
|
507
|
-
code: 'trace_start_not_found',
|
|
508
|
-
message: start.servicePath && !start.operation && !start.operationPath && !start.handler ? 'Service-only trace requires --operation or --path and will not broaden to the whole workspace' : 'No handler source matched the requested trace start selector',
|
|
509
|
-
});
|
|
497
|
+
prependTraceDiagnostic(diagnostics, selectorNotFoundDiagnostic(start));
|
|
510
498
|
const maxDepth = positiveDepth(options.depth);
|
|
511
499
|
const edges: TraceEdge[] = [];
|
|
512
500
|
const nodes = new Map<string, Record<string, unknown>>();
|
|
@@ -519,7 +507,9 @@ export function trace(
|
|
|
519
507
|
const op = operationNode(db, scope.startOperationId);
|
|
520
508
|
const impl = implementationScope(db, scope.startOperationId);
|
|
521
509
|
if (op) nodes.set(String(op.id), op);
|
|
522
|
-
const startSelection =
|
|
510
|
+
const startSelection = hintedImplementationSelection(
|
|
511
|
+
db, impl.edge, scope.startOperationId, hintOptions,
|
|
512
|
+
);
|
|
523
513
|
if (impl.edge && (impl.edge.status === 'resolved' || startSelection.methodId)) {
|
|
524
514
|
const selectedMethodId = impl.edge.status === 'resolved' ? impl.edge.to_id : startSelection.methodId;
|
|
525
515
|
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 };
|
|
@@ -539,9 +529,9 @@ export function trace(
|
|
|
539
529
|
seenScopes.add(key);
|
|
540
530
|
const calls = db
|
|
541
531
|
.prepare(
|
|
542
|
-
`SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`,
|
|
532
|
+
`SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR r.workspace_id=?) ORDER BY c.source_file,c.source_line`,
|
|
543
533
|
)
|
|
544
|
-
.all(current.repoId, current.repoId) as CallRow[];
|
|
534
|
+
.all(current.repoId, current.repoId, options.workspaceId, options.workspaceId) as CallRow[];
|
|
545
535
|
const filtered = calls.filter(
|
|
546
536
|
(c) =>
|
|
547
537
|
(!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) &&
|
|
@@ -559,7 +549,7 @@ export function trace(
|
|
|
559
549
|
const nextKey = `${nextRepoId}:${[...nextSymbols].join(',')}:${[...nextFiles].join(',')}`;
|
|
560
550
|
const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
|
|
561
551
|
if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
|
|
562
|
-
const evidence = { ...(
|
|
552
|
+
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 };
|
|
563
553
|
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 });
|
|
564
554
|
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' });
|
|
565
555
|
else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1, context: contextForSymbolCall(db, symbolCall, callerBindings) });
|
|
@@ -585,9 +575,7 @@ export function trace(
|
|
|
585
575
|
for (const row of graphRows) {
|
|
586
576
|
if (seenEdges.has(Number(row.id))) continue;
|
|
587
577
|
seenEdges.add(Number(row.id));
|
|
588
|
-
const persistedEvidence =
|
|
589
|
-
String(row.evidence_json || '{}'),
|
|
590
|
-
) as Record<string, unknown>;
|
|
578
|
+
const persistedEvidence = parseEvidence(row.evidence_json);
|
|
591
579
|
const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
|
|
592
580
|
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
|
|
593
581
|
vars: options.vars,
|
|
@@ -613,17 +601,21 @@ export function trace(
|
|
|
613
601
|
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
614
602
|
unresolvedReason: effective.unresolvedReason,
|
|
615
603
|
});
|
|
616
|
-
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
604
|
+
if ((options.dynamicMode ?? 'strict') === 'candidates'
|
|
605
|
+
&& effectiveRow.status !== 'resolved')
|
|
617
606
|
edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
|
|
618
607
|
if (effectiveRow.to_kind === 'operation') {
|
|
619
608
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
620
|
-
const contextSelection =
|
|
609
|
+
const contextSelection = contextualImplementationSelection(
|
|
610
|
+
db, implementation.edge, effectiveRow.to_id, current.repoId, evidence,
|
|
611
|
+
hintOptions,
|
|
612
|
+
);
|
|
621
613
|
const contextMethodId = contextSelection.methodId;
|
|
622
614
|
const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : undefined;
|
|
623
615
|
if (implementation.edge) {
|
|
624
|
-
const implEvidence =
|
|
625
|
-
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence
|
|
626
|
-
if (hintDiagnostic) diagnostics
|
|
616
|
+
const implEvidence = parseEvidence(implementation.edge.evidence_json);
|
|
617
|
+
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence);
|
|
618
|
+
if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
|
|
627
619
|
const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
628
620
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
629
621
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
@@ -674,6 +666,8 @@ export function trace(
|
|
|
674
666
|
}
|
|
675
667
|
}
|
|
676
668
|
const runtimeDiagnostic = runtimeVariableDiagnostic(edges);
|
|
677
|
-
if (runtimeDiagnostic) diagnostics
|
|
669
|
+
if (runtimeDiagnostic) prependTraceDiagnostic(diagnostics, runtimeDiagnostic);
|
|
670
|
+
for (const diagnostic of runtimeNoCandidateDiagnostics(edges))
|
|
671
|
+
prependTraceDiagnostic(diagnostics, diagnostic);
|
|
678
672
|
return { start, nodes: [...nodes.values()], edges, diagnostics };
|
|
679
673
|
}
|
package/src/types.ts
CHANGED
|
@@ -95,14 +95,34 @@ export interface HandlerClassFact {
|
|
|
95
95
|
sourceFile: string;
|
|
96
96
|
sourceLine: number;
|
|
97
97
|
methods: HandlerMethodFact[];
|
|
98
|
-
|
|
98
|
+
hasHandlerDecorator?: boolean;
|
|
99
|
+
classDecoratorNames?: string[];
|
|
100
|
+
observedDecoratorNames?: string[];
|
|
101
|
+
unsupportedDecoratorNames?: string[];
|
|
102
|
+
}
|
|
103
|
+
export type HandlerMethodKind =
|
|
104
|
+
| 'operation'
|
|
105
|
+
| 'entity_lifecycle'
|
|
106
|
+
| 'event'
|
|
107
|
+
| 'unsupported_lifecycle'
|
|
108
|
+
| 'unsupported_decorator';
|
|
109
|
+
export type HandlerLifecyclePhase = 'on' | 'before' | 'after';
|
|
110
|
+
export type HandlerLifecycleEvent = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE';
|
|
99
111
|
export interface HandlerMethodFact {
|
|
100
112
|
methodName: string;
|
|
101
113
|
decoratorKind: string;
|
|
102
114
|
decoratorValue?: string;
|
|
103
115
|
decoratorRawExpression: string;
|
|
116
|
+
handlerKind?: HandlerMethodKind;
|
|
117
|
+
executable?: boolean;
|
|
118
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
119
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
104
120
|
decoratorResolution: {
|
|
105
121
|
rawExpression: string;
|
|
122
|
+
decoratorExpression?: string;
|
|
123
|
+
argumentExpression?: string;
|
|
124
|
+
resolvedDecoratorKind?: string;
|
|
125
|
+
decoratorImportSource?: string;
|
|
106
126
|
resolvedValue?: string;
|
|
107
127
|
resolutionKind:
|
|
108
128
|
| 'literal'
|
|
@@ -110,8 +130,13 @@ export interface HandlerMethodFact {
|
|
|
110
130
|
| 'enum_member'
|
|
111
131
|
| 'const_object_property'
|
|
112
132
|
| 'generated_constant_name'
|
|
133
|
+
| 'lifecycle_implicit'
|
|
113
134
|
| 'unresolved';
|
|
114
135
|
unresolvedReason?: string;
|
|
136
|
+
handlerKind?: HandlerMethodKind;
|
|
137
|
+
executable?: boolean;
|
|
138
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
139
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
115
140
|
};
|
|
116
141
|
sourceFile: string;
|
|
117
142
|
sourceLine: number;
|
|
@@ -202,6 +227,7 @@ export interface ImplementationHint {
|
|
|
202
227
|
export type DynamicMode = 'strict' | 'candidates' | 'infer';
|
|
203
228
|
export interface TraceOptions {
|
|
204
229
|
depth: number;
|
|
230
|
+
workspaceId?: number;
|
|
205
231
|
vars?: Record<string, string>;
|
|
206
232
|
includeExternal?: boolean;
|
|
207
233
|
includeDb?: boolean;
|