@saptools/service-flow 0.1.50 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- 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 +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
|
@@ -1,14 +1,27 @@
|
|
|
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
|
-
import type { ImplementationHint, TraceEdge, TraceResult, TraceStart } from '../types.js';
|
|
6
|
-
import { baseTraceEvidence, edgeTarget, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
6
|
+
import type { ImplementationHint, TraceEdge, TraceOptions, TraceResult, TraceStart } from '../types.js';
|
|
7
|
+
import { baseTraceEvidence, edgeTarget, runtimeNoCandidateDiagnostics, runtimeResolution, runtimeVariableDiagnostic, type TraceGraphRow } from './evidence.js';
|
|
8
|
+
import { dynamicCandidateBranches } from './dynamic-branches.js';
|
|
9
|
+
import {
|
|
10
|
+
loadTraceDiagnostics,
|
|
11
|
+
prependTraceDiagnostic,
|
|
12
|
+
} from './002-trace-diagnostics.js';
|
|
7
13
|
import { implementationHintDiagnostic, selectImplementation, type ImplementationSelection } from './implementation-hints.js';
|
|
8
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
ambiguousStartDiagnostic,
|
|
16
|
+
selectorNotFoundDiagnostic,
|
|
17
|
+
selectorRepoAmbiguousDiagnostic,
|
|
18
|
+
selectorRepoNotFoundDiagnostic,
|
|
19
|
+
sourceScopeForSelector,
|
|
20
|
+
} from './selectors.js';
|
|
9
21
|
interface RepoRef {
|
|
10
22
|
id: number;
|
|
11
23
|
name: string;
|
|
24
|
+
packageName?: string;
|
|
12
25
|
}
|
|
13
26
|
interface StartScope {
|
|
14
27
|
repo?: RepoRef;
|
|
@@ -77,13 +90,14 @@ function normalizeOperation(value: string | undefined): string | undefined {
|
|
|
77
90
|
function positiveDepth(value: number): number {
|
|
78
91
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 25;
|
|
79
92
|
}
|
|
80
|
-
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 {
|
|
93
|
+
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 {
|
|
81
94
|
const requested = normalizeOperation(start.operationPath ?? start.operation);
|
|
82
95
|
if (!requested) return undefined;
|
|
83
96
|
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
|
|
84
97
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
85
|
-
WHERE (? IS NULL OR r.
|
|
86
|
-
|
|
98
|
+
WHERE (? IS NULL OR r.workspace_id=?) AND (? IS NULL OR r.id=?)
|
|
99
|
+
AND (? IS NULL OR s.service_path=?) AND (o.operation_name=? OR o.operation_path=? OR o.operation_path=?)
|
|
100
|
+
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>>;
|
|
87
101
|
if (rows.length === 0) return undefined;
|
|
88
102
|
const repoCount = new Set(rows.map((row) => String(row.repoName))).size;
|
|
89
103
|
const serviceCount = new Set(rows.map((row) => `${String(row.repoName)}:${String(row.servicePath)}`)).size;
|
|
@@ -125,60 +139,33 @@ function sourceFilesForStart(
|
|
|
125
139
|
db: Db,
|
|
126
140
|
repoId: number | undefined,
|
|
127
141
|
start: TraceStart,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
operation,
|
|
153
|
-
operation,
|
|
154
|
-
operation,
|
|
155
|
-
operation,
|
|
156
|
-
) as Array<{ sourceFile?: string; symbolId?: number }>;
|
|
157
|
-
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 };
|
|
158
|
-
if (start.servicePath && operation) {
|
|
159
|
-
const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
|
|
160
|
-
FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
|
|
161
|
-
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)
|
|
162
|
-
JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
|
|
163
|
-
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
164
|
-
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
|
|
165
|
-
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 }>;
|
|
166
|
-
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 };
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions): StartScope {
|
|
171
|
-
const repo = start.repo
|
|
172
|
-
? (db
|
|
173
|
-
.prepare(
|
|
174
|
-
'SELECT id,name FROM repositories WHERE name=? OR package_name=?',
|
|
175
|
-
)
|
|
176
|
-
.get(start.repo, start.repo) as RepoRef | undefined)
|
|
177
|
-
: undefined;
|
|
178
|
-
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
179
|
-
const operationScope = operationStartScope(db, repo?.id, start, hintOptions);
|
|
142
|
+
workspaceId: number | undefined,
|
|
143
|
+
): ReturnType<typeof sourceScopeForSelector> {
|
|
144
|
+
return sourceScopeForSelector(db, repoId, start, workspaceId);
|
|
145
|
+
}
|
|
146
|
+
function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOptions, workspaceId?: number): StartScope {
|
|
147
|
+
const repos: RepoRef[] = start.repo
|
|
148
|
+
? reposByName(db, start.repo, workspaceId).map((row) => ({
|
|
149
|
+
id: row.id,
|
|
150
|
+
name: row.name,
|
|
151
|
+
packageName: row.package_name ?? undefined,
|
|
152
|
+
}))
|
|
153
|
+
: [];
|
|
154
|
+
if (start.repo && repos.length === 0) return {
|
|
155
|
+
selectorMatched: false,
|
|
156
|
+
startDiagnostics: [selectorRepoNotFoundDiagnostic(start.repo)],
|
|
157
|
+
};
|
|
158
|
+
if (start.repo && repos.length > 1) return {
|
|
159
|
+
selectorMatched: false,
|
|
160
|
+
startDiagnostics: [selectorRepoAmbiguousDiagnostic(start.repo, repos)],
|
|
161
|
+
};
|
|
162
|
+
const repo = repos[0];
|
|
163
|
+
const operationScope = operationStartScope(
|
|
164
|
+
db, repo?.id, start, hintOptions, workspaceId,
|
|
165
|
+
);
|
|
180
166
|
const terminalOperationScope = operationScope && !operationScope.files && (operationScope.diagnostics ?? []).some((d) => d.resolutionStage === 'operation' || d.resolutionStage === 'implementation');
|
|
181
|
-
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start);
|
|
167
|
+
const sourceScope = operationScope?.files || terminalOperationScope ? operationScope : sourceFilesForStart(db, repo?.id, start, workspaceId);
|
|
168
|
+
const terminalSelectorScope = Boolean(sourceScope?.diagnostics?.length && !sourceScope.files);
|
|
182
169
|
const sourceFiles = sourceScope?.files;
|
|
183
170
|
const hasSelector = Boolean(
|
|
184
171
|
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath,
|
|
@@ -190,9 +177,12 @@ function startScope(db: Db, start: TraceStart, hintOptions: ImplementationHintOp
|
|
|
190
177
|
executionRepoId: sourceScope?.repoId ?? repo?.id,
|
|
191
178
|
sourceFiles,
|
|
192
179
|
symbolIds: sourceScope?.symbols,
|
|
193
|
-
selectorMatched: !terminalOperationScope &&
|
|
180
|
+
selectorMatched: !terminalOperationScope && !terminalSelectorScope
|
|
181
|
+
&& (!hasSelector || sourceFiles !== undefined),
|
|
194
182
|
startOperationId: operationScope?.operationId,
|
|
195
|
-
startDiagnostics: operationScope?.diagnostics
|
|
183
|
+
startDiagnostics: operationScope?.diagnostics?.length
|
|
184
|
+
? operationScope.diagnostics
|
|
185
|
+
: sourceScope?.diagnostics,
|
|
196
186
|
};
|
|
197
187
|
}
|
|
198
188
|
function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
|
|
@@ -210,8 +200,19 @@ function handlerFilesForOperation(db: Db, operationId: string): Set<string> {
|
|
|
210
200
|
.prepare(
|
|
211
201
|
`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
|
|
212
202
|
JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
213
|
-
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
|
|
214
|
-
|
|
203
|
+
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id
|
|
204
|
+
AND sym.source_file=hc.source_file
|
|
205
|
+
AND sym.qualified_name=hc.class_name || '.' || hm.method_name
|
|
206
|
+
AND sym.start_line=hm.source_line
|
|
207
|
+
WHERE hc.repo_id=?
|
|
208
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
|
|
209
|
+
CASE WHEN hm.decorator_kind='Event' THEN 'event'
|
|
210
|
+
WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
|
|
211
|
+
ELSE 'unsupported' END)='operation'
|
|
212
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
|
|
213
|
+
CASE WHEN hm.decorator_kind IN ('Action','Func','On')
|
|
214
|
+
THEN 1 ELSE 0 END)=1
|
|
215
|
+
AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`,
|
|
215
216
|
)
|
|
216
217
|
.all(op.repoId, operation, operation, op.operationName) as Array<{
|
|
217
218
|
sourceFile?: string;
|
|
@@ -229,7 +230,9 @@ function handlerMethodNode(db: Db, methodId: string): Record<string, unknown> |
|
|
|
229
230
|
function implementationScope(db: Db, operationId: string): { repoId?: number; files: Set<string>; symbolId?: number; edge?: GraphRow } {
|
|
230
231
|
const edge = implementationEdge(db, operationId);
|
|
231
232
|
if (!edge || edge.status !== 'resolved') return { files: new Set(), edge };
|
|
232
|
-
const row = db.prepare(
|
|
233
|
+
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;
|
|
234
|
+
if (!row || typeof row.symbolId !== 'number')
|
|
235
|
+
return { repoId: row?.repoId, files: new Set(), edge };
|
|
233
236
|
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
234
237
|
}
|
|
235
238
|
function implementationMethodIdFromHint(edge: GraphRow | undefined, options: ImplementationHintOptions): ImplementationSelection {
|
|
@@ -256,8 +259,8 @@ function contextImplementationMethodId(edge: GraphRow | undefined, callerRepoId:
|
|
|
256
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 } };
|
|
257
260
|
}
|
|
258
261
|
function handlerScope(db: Db, methodId: string): { repoId?: number; files: Set<string>; symbolId?: number } | undefined {
|
|
259
|
-
const row = db.prepare(
|
|
260
|
-
if (!row) return undefined;
|
|
262
|
+
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
|
+
if (!row || typeof row.symbolId !== 'number') return undefined;
|
|
261
264
|
return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
|
|
262
265
|
}
|
|
263
266
|
|
|
@@ -307,6 +310,7 @@ function operationNode(db: Db, operationId: string): Record<string, unknown> | u
|
|
|
307
310
|
if (!row) return undefined;
|
|
308
311
|
return { id: `operation:${operationId}`, kind: 'operation', label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
|
|
309
312
|
}
|
|
313
|
+
|
|
310
314
|
function workspaceIdForCall(db: Db, callId: string): number | undefined {
|
|
311
315
|
return (db.prepare('SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?').get(callId) as { workspaceId?: number } | undefined)?.workspaceId;
|
|
312
316
|
}
|
|
@@ -486,33 +490,28 @@ function contextualRuntimeResolution(db: Db, call: CallRow, binding: ContextBind
|
|
|
486
490
|
export function trace(
|
|
487
491
|
db: Db,
|
|
488
492
|
start: TraceStart,
|
|
489
|
-
options:
|
|
490
|
-
depth: number;
|
|
491
|
-
vars?: Record<string, string>;
|
|
492
|
-
includeExternal?: boolean;
|
|
493
|
-
includeDb?: boolean;
|
|
494
|
-
includeAsync?: boolean;
|
|
495
|
-
implementationRepo?: string;
|
|
496
|
-
implementationHints?: ImplementationHint[];
|
|
497
|
-
},
|
|
493
|
+
options: TraceOptions,
|
|
498
494
|
): TraceResult {
|
|
499
495
|
const hintOptions = { implementationRepo: options.implementationRepo, implementationHints: options.implementationHints };
|
|
500
|
-
const scope = startScope(db, start, hintOptions);
|
|
501
|
-
const
|
|
502
|
-
.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
496
|
+
const scope = startScope(db, start, hintOptions, options.workspaceId);
|
|
497
|
+
const hasSelector = Boolean(start.repo || start.handler || start.operation
|
|
498
|
+
|| start.operationPath || start.servicePath);
|
|
499
|
+
const diagnosticRepoId = scope.executionRepoId ?? scope.repo?.id;
|
|
500
|
+
const diagnostics = loadTraceDiagnostics(
|
|
501
|
+
db,
|
|
502
|
+
diagnosticRepoId,
|
|
503
|
+
!hasSelector,
|
|
504
|
+
options.workspaceId,
|
|
505
|
+
);
|
|
506
|
+
const stale = diagnosticRepoId !== undefined || !hasSelector
|
|
507
|
+
? 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 }>
|
|
508
|
+
: [];
|
|
507
509
|
for (const row of stale)
|
|
508
|
-
diagnostics
|
|
509
|
-
for (const diagnostic of scope.startDiagnostics ?? [])
|
|
510
|
+
prependTraceDiagnostic(diagnostics, { severity: 'warning', code: 'graph_stale', message: `Graph is stale for ${row.name ?? 'repository'}: ${row.reason ?? 'facts_changed'}. Run service-flow link.` });
|
|
511
|
+
for (const diagnostic of scope.startDiagnostics ?? [])
|
|
512
|
+
prependTraceDiagnostic(diagnostics, diagnostic);
|
|
510
513
|
if (!scope.selectorMatched && !(scope.startDiagnostics?.length))
|
|
511
|
-
diagnostics
|
|
512
|
-
severity: 'warning',
|
|
513
|
-
code: 'trace_start_not_found',
|
|
514
|
-
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',
|
|
515
|
-
});
|
|
514
|
+
prependTraceDiagnostic(diagnostics, selectorNotFoundDiagnostic(start));
|
|
516
515
|
const maxDepth = positiveDepth(options.depth);
|
|
517
516
|
const edges: TraceEdge[] = [];
|
|
518
517
|
const nodes = new Map<string, Record<string, unknown>>();
|
|
@@ -545,9 +544,9 @@ export function trace(
|
|
|
545
544
|
seenScopes.add(key);
|
|
546
545
|
const calls = db
|
|
547
546
|
.prepare(
|
|
548
|
-
`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`,
|
|
547
|
+
`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`,
|
|
549
548
|
)
|
|
550
|
-
.all(current.repoId, current.repoId) as CallRow[];
|
|
549
|
+
.all(current.repoId, current.repoId, options.workspaceId, options.workspaceId) as CallRow[];
|
|
551
550
|
const filtered = calls.filter(
|
|
552
551
|
(c) =>
|
|
553
552
|
(!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) &&
|
|
@@ -595,7 +594,11 @@ export function trace(
|
|
|
595
594
|
String(row.evidence_json || '{}'),
|
|
596
595
|
) as Record<string, unknown>;
|
|
597
596
|
const rawEvidence = baseTraceEvidence(row as TraceGraphRow, call, persistedEvidence, contextual.evidence);
|
|
598
|
-
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence,
|
|
597
|
+
const effective = runtimeResolution(db, row as TraceGraphRow, rawEvidence, {
|
|
598
|
+
vars: options.vars,
|
|
599
|
+
dynamicMode: options.dynamicMode ?? 'strict',
|
|
600
|
+
maxDynamicCandidates: options.maxDynamicCandidates,
|
|
601
|
+
}, workspaceIdForCall(db, String(call.id)), contextual.unresolvedReason);
|
|
599
602
|
const evidence = effective.evidence;
|
|
600
603
|
const effectiveRow = effective.row;
|
|
601
604
|
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
@@ -615,6 +618,8 @@ export function trace(
|
|
|
615
618
|
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
616
619
|
unresolvedReason: effective.unresolvedReason,
|
|
617
620
|
});
|
|
621
|
+
if ((options.dynamicMode ?? 'strict') === 'candidates')
|
|
622
|
+
edges.push(...dynamicCandidateBranches(current.depth, call, evidence));
|
|
618
623
|
if (effectiveRow.to_kind === 'operation') {
|
|
619
624
|
const implementation = implementationScope(db, effectiveRow.to_id);
|
|
620
625
|
const contextSelection = contextImplementationMethodId(implementation.edge, current.repoId, evidence, hintOptions);
|
|
@@ -623,7 +628,7 @@ export function trace(
|
|
|
623
628
|
if (implementation.edge) {
|
|
624
629
|
const implEvidence = JSON.parse(String(implementation.edge.evidence_json || '{}')) as Record<string, unknown>;
|
|
625
630
|
const hintDiagnostic = implementationHintDiagnostic(contextSelection, implEvidence.implementationHintSuggestions);
|
|
626
|
-
if (hintDiagnostic) diagnostics
|
|
631
|
+
if (hintDiagnostic) prependTraceDiagnostic(diagnostics, hintDiagnostic);
|
|
627
632
|
const handlerNode = implementation.edge.status === 'resolved' ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
|
|
628
633
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
629
634
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
@@ -674,6 +679,8 @@ export function trace(
|
|
|
674
679
|
}
|
|
675
680
|
}
|
|
676
681
|
const runtimeDiagnostic = runtimeVariableDiagnostic(edges);
|
|
677
|
-
if (runtimeDiagnostic) diagnostics
|
|
682
|
+
if (runtimeDiagnostic) prependTraceDiagnostic(diagnostics, runtimeDiagnostic);
|
|
683
|
+
for (const diagnostic of runtimeNoCandidateDiagnostics(edges))
|
|
684
|
+
prependTraceDiagnostic(diagnostics, diagnostic);
|
|
678
685
|
return { start, nodes: [...nodes.values()], edges, diagnostics };
|
|
679
686
|
}
|
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;
|
|
@@ -199,6 +224,19 @@ export interface ImplementationHint {
|
|
|
199
224
|
candidateFamily?: string;
|
|
200
225
|
implementationRepo: string;
|
|
201
226
|
}
|
|
227
|
+
export type DynamicMode = 'strict' | 'candidates' | 'infer';
|
|
228
|
+
export interface TraceOptions {
|
|
229
|
+
depth: number;
|
|
230
|
+
workspaceId?: number;
|
|
231
|
+
vars?: Record<string, string>;
|
|
232
|
+
includeExternal?: boolean;
|
|
233
|
+
includeDb?: boolean;
|
|
234
|
+
includeAsync?: boolean;
|
|
235
|
+
implementationRepo?: string;
|
|
236
|
+
implementationHints?: ImplementationHint[];
|
|
237
|
+
dynamicMode?: DynamicMode;
|
|
238
|
+
maxDynamicCandidates?: number;
|
|
239
|
+
}
|
|
202
240
|
export interface TraceEdge {
|
|
203
241
|
step: number;
|
|
204
242
|
type: string;
|