@saptools/service-flow 0.1.65 → 0.1.67
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 +16 -0
- package/README.md +67 -11
- package/TECHNICAL-NOTE.md +41 -1
- package/dist/{chunk-OONNRIDL.js → chunk-ZQABU7MR.js} +3764 -643
- package/dist/chunk-ZQABU7MR.js.map +1 -0
- package/dist/cli.js +158 -295
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +196 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/002-doctor-lifecycle.ts +66 -0
- package/src/cli/doctor.ts +14 -27
- package/src/cli.ts +130 -102
- package/src/db/000-call-fact-repository.ts +537 -0
- package/src/db/001-fact-lifecycle.ts +111 -0
- package/src/db/migrations.ts +16 -1
- package/src/db/repositories.ts +1 -315
- package/src/db/schema.ts +4 -2
- package/src/index.ts +22 -0
- package/src/linker/004-event-subscription-handler-linker.ts +300 -0
- package/src/linker/cross-repo-linker.ts +23 -2
- package/src/output/001-compact-json-output.ts +6 -0
- package/src/parsers/imported-wrapper-parser.ts +4 -0
- package/src/parsers/outbound-call-parser.ts +11 -1
- package/src/parsers/symbol-parser.ts +7 -4
- package/src/trace/010-traversal-scope.ts +188 -0
- package/src/trace/011-event-subscriber-traversal.ts +366 -0
- package/src/trace/012-trace-graph-lookups.ts +74 -0
- package/src/trace/013-trace-root-scopes.ts +279 -0
- package/src/trace/014-compact-contract.ts +347 -0
- package/src/trace/015-trace-edge-recorder.ts +336 -0
- package/src/trace/016-compact-projector.ts +697 -0
- package/src/trace/017-trace-context.ts +378 -0
- package/src/trace/018-compact-trace.ts +87 -0
- package/src/trace/019-trace-edge-semantics.ts +328 -0
- package/src/trace/020-compact-field-projection.ts +387 -0
- package/src/trace/dynamic-branches.ts +35 -13
- package/src/trace/trace-engine.ts +271 -245
- package/src/types.ts +10 -0
- package/src/version.ts +1 -1
- package/dist/chunk-OONNRIDL.js.map +0 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { extractPlaceholders } from '../linker/dynamic-edge-resolver.js';
|
|
3
|
+
import { boundCandidateLikeEvidence } from '../utils/000-bounded-projection.js';
|
|
4
|
+
import type { ContextBinding } from './008-contextual-runtime-state.js';
|
|
5
|
+
|
|
6
|
+
export interface TraceContextCall extends Record<string, unknown> {
|
|
7
|
+
service_binding_id?: number;
|
|
8
|
+
evidence_json?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type BindingRow = Omit<ContextBinding,
|
|
12
|
+
'bindingId' | 'source' | 'calleeReceiver'> & {
|
|
13
|
+
id?: number;
|
|
14
|
+
symbolId?: number | null;
|
|
15
|
+
variableName?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function parseTraceEvidence(value: unknown): Record<string, unknown> {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(String(value || '{}')) as unknown;
|
|
21
|
+
return isRecord(parsed) ? boundCandidateLikeEvidence(parsed) : {};
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function receiverFromTraceEvidence(value: unknown): string | undefined {
|
|
28
|
+
const evidence = parseTraceEvidence(value);
|
|
29
|
+
return typeof evidence.receiver === 'string' ? evidence.receiver : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function knownBindingsForCalls(
|
|
33
|
+
db: Db,
|
|
34
|
+
calls: TraceContextCall[],
|
|
35
|
+
): Map<string, ContextBinding> {
|
|
36
|
+
const map = new Map<string, ContextBinding>();
|
|
37
|
+
for (const call of calls) addCallBinding(db, map, call);
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function knownBindingsForScope(
|
|
42
|
+
db: Db,
|
|
43
|
+
repoId: number | undefined,
|
|
44
|
+
symbolIds: Set<number> | undefined,
|
|
45
|
+
files: Set<string> | undefined,
|
|
46
|
+
): Map<string, ContextBinding> {
|
|
47
|
+
const map = new Map<string, ContextBinding>();
|
|
48
|
+
if (repoId === undefined) return map;
|
|
49
|
+
const rows = db.prepare(`SELECT b.id,b.symbol_id symbolId,
|
|
50
|
+
b.variable_name variableName,b.alias,b.alias_expr aliasExpr,
|
|
51
|
+
b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
|
|
52
|
+
b.source_file sourceFile,b.source_line sourceLine,
|
|
53
|
+
req.service_path requireServicePath,req.destination requireDestination
|
|
54
|
+
FROM service_bindings b LEFT JOIN cds_requires req
|
|
55
|
+
ON req.repo_id=b.repo_id AND req.alias=b.alias
|
|
56
|
+
WHERE b.repo_id=?
|
|
57
|
+
ORDER BY b.source_file COLLATE BINARY,b.source_line,b.id`).all(
|
|
58
|
+
repoId,
|
|
59
|
+
) as BindingRow[];
|
|
60
|
+
for (const row of rows) addScopeBinding(map, row, symbolIds, files);
|
|
61
|
+
return map;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function contextForSymbolCall(
|
|
65
|
+
db: Db,
|
|
66
|
+
symbolCall: Record<string, unknown>,
|
|
67
|
+
callerBindings: Map<string, ContextBinding>,
|
|
68
|
+
): Map<string, ContextBinding> {
|
|
69
|
+
const next = new Map<string, ContextBinding>();
|
|
70
|
+
if (callerBindings.size === 0) return next;
|
|
71
|
+
const context = symbolCallContext(db, symbolCall);
|
|
72
|
+
context.args.forEach((argument, index) => addArgumentBindings(
|
|
73
|
+
next, callerBindings, context, argument, index,
|
|
74
|
+
));
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addCallBinding(
|
|
79
|
+
db: Db,
|
|
80
|
+
map: Map<string, ContextBinding>,
|
|
81
|
+
call: TraceContextCall,
|
|
82
|
+
): void {
|
|
83
|
+
const receiver = receiverFromTraceEvidence(call.evidence_json);
|
|
84
|
+
const bindingId = Number(call.service_binding_id ?? 0);
|
|
85
|
+
if (!receiver || !bindingId) return;
|
|
86
|
+
const row = db.prepare(`SELECT b.id,b.alias,b.alias_expr aliasExpr,
|
|
87
|
+
b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
|
|
88
|
+
b.source_file sourceFile,b.source_line sourceLine,
|
|
89
|
+
req.service_path requireServicePath,req.destination requireDestination
|
|
90
|
+
FROM service_bindings b LEFT JOIN cds_requires req
|
|
91
|
+
ON req.repo_id=b.repo_id AND req.alias=b.alias WHERE b.id=?`).get(
|
|
92
|
+
bindingId,
|
|
93
|
+
) as ContextBinding | undefined;
|
|
94
|
+
if (row) map.set(receiver, enrichBinding({
|
|
95
|
+
...row, bindingId, source: 'local_service_binding', calleeReceiver: receiver,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function addScopeBinding(
|
|
100
|
+
map: Map<string, ContextBinding>,
|
|
101
|
+
row: BindingRow,
|
|
102
|
+
symbolIds: Set<number> | undefined,
|
|
103
|
+
files: Set<string> | undefined,
|
|
104
|
+
): void {
|
|
105
|
+
if (!row.variableName) return;
|
|
106
|
+
if (files && !files.has(String(row.sourceFile))) return;
|
|
107
|
+
if (symbolIds?.size && row.symbolId != null
|
|
108
|
+
&& !symbolIds.has(Number(row.symbolId))) return;
|
|
109
|
+
const candidate = enrichBinding({
|
|
110
|
+
...row, bindingId: Number(row.id), source: 'local_service_binding',
|
|
111
|
+
calleeReceiver: row.variableName, resolutionStatus: 'selected',
|
|
112
|
+
});
|
|
113
|
+
const existing = map.get(row.variableName);
|
|
114
|
+
if (!existing) {
|
|
115
|
+
map.set(row.variableName, candidate);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
map.set(row.variableName, ambiguousBinding(existing, candidate));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ambiguousBinding(
|
|
122
|
+
existing: ContextBinding,
|
|
123
|
+
candidate: ContextBinding,
|
|
124
|
+
): ContextBinding {
|
|
125
|
+
const bindingCandidates = uniqueBindingCandidates([
|
|
126
|
+
...(existing.bindingCandidates ?? [bindingEvidence(existing)]),
|
|
127
|
+
bindingEvidence(candidate),
|
|
128
|
+
]);
|
|
129
|
+
return {
|
|
130
|
+
...candidate, bindingId: undefined,
|
|
131
|
+
source: 'ambiguous_local_service_bindings', resolutionStatus: 'ambiguous',
|
|
132
|
+
bindingCandidates,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface SymbolCallContext {
|
|
137
|
+
args: Array<Record<string, unknown>>;
|
|
138
|
+
params: string[];
|
|
139
|
+
parameterBindings: Array<Record<string, unknown>>;
|
|
140
|
+
parameterPropertyAliases: Array<Record<string, unknown>>;
|
|
141
|
+
provenance: Record<string, unknown>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function symbolCallContext(
|
|
145
|
+
db: Db,
|
|
146
|
+
symbolCall: Record<string, unknown>,
|
|
147
|
+
): SymbolCallContext {
|
|
148
|
+
const callEvidence = parseTraceEvidence(symbolCall.evidence_json);
|
|
149
|
+
const callee = db.prepare(`SELECT evidence_json evidenceJson,
|
|
150
|
+
source_file sourceFile,start_line startLine FROM symbols WHERE id=?`).get(
|
|
151
|
+
symbolCall.callee_symbol_id,
|
|
152
|
+
) as { evidenceJson?: string; sourceFile?: string; startLine?: number } | undefined;
|
|
153
|
+
const evidence = parseTraceEvidence(callee?.evidenceJson);
|
|
154
|
+
return {
|
|
155
|
+
args: recordArray(callEvidence.callArguments),
|
|
156
|
+
params: stringArray(evidence.parameters),
|
|
157
|
+
parameterBindings: recordArray(evidence.parameterBindings),
|
|
158
|
+
parameterPropertyAliases: recordArray(evidence.parameterPropertyAliases),
|
|
159
|
+
provenance: {
|
|
160
|
+
callerSite: { sourceFile: String(symbolCall.source_file ?? ''),
|
|
161
|
+
sourceLine: Number(symbolCall.source_line ?? 0) },
|
|
162
|
+
calleeSite: { sourceFile: callee?.sourceFile, sourceLine: callee?.startLine },
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addArgumentBindings(
|
|
168
|
+
next: Map<string, ContextBinding>,
|
|
169
|
+
caller: Map<string, ContextBinding>,
|
|
170
|
+
context: SymbolCallContext,
|
|
171
|
+
argument: Record<string, unknown>,
|
|
172
|
+
index: number,
|
|
173
|
+
): void {
|
|
174
|
+
const parameterBinding = context.parameterBindings.find(
|
|
175
|
+
(binding) => binding.index === index,
|
|
176
|
+
);
|
|
177
|
+
const parameter = parameterBinding?.kind === 'identifier'
|
|
178
|
+
&& typeof parameterBinding.name === 'string'
|
|
179
|
+
? parameterBinding.name : context.params[index];
|
|
180
|
+
addIdentifierBinding(next, caller, context.provenance, argument, parameter);
|
|
181
|
+
addObjectBindings(next, caller, context, argument, parameterBinding, parameter, index);
|
|
182
|
+
addArrayBindings(next, caller, context.provenance, argument, parameterBinding, index);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function addIdentifierBinding(
|
|
186
|
+
next: Map<string, ContextBinding>,
|
|
187
|
+
caller: Map<string, ContextBinding>,
|
|
188
|
+
provenance: Record<string, unknown>,
|
|
189
|
+
argument: Record<string, unknown>,
|
|
190
|
+
parameter: string | undefined,
|
|
191
|
+
): void {
|
|
192
|
+
if (argument.kind !== 'identifier' || typeof argument.name !== 'string'
|
|
193
|
+
|| !parameter) return;
|
|
194
|
+
const binding = caller.get(argument.name);
|
|
195
|
+
if (binding) next.set(parameter, { ...binding, ...provenance,
|
|
196
|
+
source: 'local_symbol_argument', callerArgument: argument.name,
|
|
197
|
+
calleeParameter: parameter, calleeReceiver: parameter });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function addObjectBindings(
|
|
201
|
+
next: Map<string, ContextBinding>,
|
|
202
|
+
caller: Map<string, ContextBinding>,
|
|
203
|
+
context: SymbolCallContext,
|
|
204
|
+
argument: Record<string, unknown>,
|
|
205
|
+
parameterBinding: Record<string, unknown> | undefined,
|
|
206
|
+
parameter: string | undefined,
|
|
207
|
+
index: number,
|
|
208
|
+
): void {
|
|
209
|
+
if (argument.kind !== 'object_literal' || !Array.isArray(argument.properties)) return;
|
|
210
|
+
for (const property of recordArray(argument.properties)) addObjectPropertyBinding(
|
|
211
|
+
next, caller, context, property, parameterBinding, parameter, index,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function addObjectPropertyBinding(
|
|
216
|
+
next: Map<string, ContextBinding>,
|
|
217
|
+
caller: Map<string, ContextBinding>,
|
|
218
|
+
context: SymbolCallContext,
|
|
219
|
+
property: Record<string, unknown>,
|
|
220
|
+
parameterBinding: Record<string, unknown> | undefined,
|
|
221
|
+
parameter: string | undefined,
|
|
222
|
+
index: number,
|
|
223
|
+
): void {
|
|
224
|
+
if (typeof property.property !== 'string' || typeof property.argument !== 'string') return;
|
|
225
|
+
const binding = caller.get(property.argument);
|
|
226
|
+
if (!binding) return;
|
|
227
|
+
const local = objectPatternLocal(parameterBinding, property.property);
|
|
228
|
+
if (local) {
|
|
229
|
+
next.set(local, objectBinding(binding, context.provenance,
|
|
230
|
+
property.property, property.argument, String(index), local,
|
|
231
|
+
'local_symbol_destructured_object_argument'));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!parameter) return;
|
|
235
|
+
const receiver = `${parameter}.${property.property}`;
|
|
236
|
+
next.set(receiver, objectBinding(binding, context.provenance,
|
|
237
|
+
property.property, property.argument, parameter, receiver,
|
|
238
|
+
'local_symbol_object_argument'));
|
|
239
|
+
addObjectAliases(next, binding, context, property, parameter, receiver);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addObjectAliases(
|
|
243
|
+
next: Map<string, ContextBinding>,
|
|
244
|
+
binding: ContextBinding,
|
|
245
|
+
context: SymbolCallContext,
|
|
246
|
+
property: Record<string, unknown>,
|
|
247
|
+
parameter: string,
|
|
248
|
+
receiver: string,
|
|
249
|
+
): void {
|
|
250
|
+
for (const alias of context.parameterPropertyAliases) {
|
|
251
|
+
if (alias.parameter !== parameter || alias.property !== property.property
|
|
252
|
+
|| typeof alias.local !== 'string') continue;
|
|
253
|
+
next.set(alias.local, {
|
|
254
|
+
...objectBinding(binding, context.provenance,
|
|
255
|
+
String(property.property), String(property.argument), parameter,
|
|
256
|
+
alias.local, 'local_symbol_object_parameter_destructure'),
|
|
257
|
+
calleeObjectProperty: receiver,
|
|
258
|
+
calleeLocalDestructuredIdentifier: alias.local,
|
|
259
|
+
parameterPropertyAliasKind: alias.kind,
|
|
260
|
+
parameterPropertyAliasLine: alias.line,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function addArrayBindings(
|
|
266
|
+
next: Map<string, ContextBinding>,
|
|
267
|
+
caller: Map<string, ContextBinding>,
|
|
268
|
+
provenance: Record<string, unknown>,
|
|
269
|
+
argument: Record<string, unknown>,
|
|
270
|
+
parameterBinding: Record<string, unknown> | undefined,
|
|
271
|
+
index: number,
|
|
272
|
+
): void {
|
|
273
|
+
const arrays = arrayPattern(argument, parameterBinding);
|
|
274
|
+
if (!arrays) return;
|
|
275
|
+
for (const element of arrays.elements)
|
|
276
|
+
addArrayElement(next, caller, provenance, element, arrays.targets, index);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function arrayPattern(
|
|
280
|
+
argument: Record<string, unknown>,
|
|
281
|
+
binding: Record<string, unknown> | undefined,
|
|
282
|
+
): { elements: Array<Record<string, unknown>>;
|
|
283
|
+
targets: Array<Record<string, unknown>> } | undefined {
|
|
284
|
+
if (argument.kind !== 'array_literal') return undefined;
|
|
285
|
+
if (!Array.isArray(argument.elements)) return undefined;
|
|
286
|
+
if (binding?.kind !== 'array_pattern') return undefined;
|
|
287
|
+
if (!Array.isArray(binding.elements)) return undefined;
|
|
288
|
+
return { elements: recordArray(argument.elements),
|
|
289
|
+
targets: recordArray(binding.elements) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function addArrayElement(
|
|
293
|
+
next: Map<string, ContextBinding>,
|
|
294
|
+
caller: Map<string, ContextBinding>,
|
|
295
|
+
provenance: Record<string, unknown>,
|
|
296
|
+
element: Record<string, unknown>,
|
|
297
|
+
targets: Array<Record<string, unknown>>,
|
|
298
|
+
index: number,
|
|
299
|
+
): void {
|
|
300
|
+
if (element.kind !== 'identifier' || typeof element.name !== 'string') return;
|
|
301
|
+
const target = targets.find((item) => item.index === element.index);
|
|
302
|
+
if (typeof target?.local !== 'string') return;
|
|
303
|
+
const binding = caller.get(element.name);
|
|
304
|
+
if (binding) next.set(target.local, { ...binding, ...provenance,
|
|
305
|
+
source: 'local_symbol_destructured_array_argument',
|
|
306
|
+
callerArgument: element.name, calleeParameter: String(index),
|
|
307
|
+
calleeReceiver: target.local });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function objectPatternLocal(
|
|
311
|
+
binding: Record<string, unknown> | undefined,
|
|
312
|
+
property: string,
|
|
313
|
+
): string | undefined {
|
|
314
|
+
if (binding?.kind !== 'object_pattern' || !Array.isArray(binding.properties)) return undefined;
|
|
315
|
+
const match = recordArray(binding.properties).find(
|
|
316
|
+
(item) => item.property === property && typeof item.local === 'string',
|
|
317
|
+
);
|
|
318
|
+
return typeof match?.local === 'string' ? match.local : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function objectBinding(
|
|
322
|
+
binding: ContextBinding,
|
|
323
|
+
provenance: Record<string, unknown>,
|
|
324
|
+
callerProperty: string,
|
|
325
|
+
callerArgument: string,
|
|
326
|
+
parameter: string,
|
|
327
|
+
receiver: string,
|
|
328
|
+
source: string,
|
|
329
|
+
): ContextBinding {
|
|
330
|
+
return { ...binding, ...provenance, source,
|
|
331
|
+
callerProperty, callerArgument,
|
|
332
|
+
calleeParameter: parameter, calleeReceiver: receiver };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function enrichBinding(row: ContextBinding): ContextBinding {
|
|
336
|
+
const servicePath = row.servicePathExpr && !hasPlaceholder(row.servicePathExpr)
|
|
337
|
+
? row.servicePathExpr : !row.servicePathExpr ? row.requireServicePath : undefined;
|
|
338
|
+
const destination = row.destinationExpr && !hasPlaceholder(row.destinationExpr)
|
|
339
|
+
? row.destinationExpr : !row.destinationExpr ? row.requireDestination : undefined;
|
|
340
|
+
return { ...row, effectiveServicePath: servicePath,
|
|
341
|
+
effectiveDestination: destination };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasPlaceholder(value: string | undefined): boolean {
|
|
345
|
+
return extractPlaceholders(value).length > 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function bindingEvidence(binding: ContextBinding): Record<string, unknown> {
|
|
349
|
+
return { bindingId: binding.bindingId, sourceFile: binding.sourceFile,
|
|
350
|
+
sourceLine: binding.sourceLine, alias: binding.alias,
|
|
351
|
+
aliasExpr: binding.aliasExpr, destinationExpr: binding.destinationExpr,
|
|
352
|
+
servicePathExpr: binding.servicePathExpr };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function uniqueBindingCandidates(
|
|
356
|
+
candidates: Array<Record<string, unknown>>,
|
|
357
|
+
): Array<Record<string, unknown>> {
|
|
358
|
+
const seen = new Set<string>();
|
|
359
|
+
return candidates.filter((candidate) => {
|
|
360
|
+
const key = JSON.stringify(candidate);
|
|
361
|
+
if (seen.has(key)) return false;
|
|
362
|
+
seen.add(key);
|
|
363
|
+
return true;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function recordArray(value: unknown): Array<Record<string, unknown>> {
|
|
368
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function stringArray(value: unknown): string[] {
|
|
372
|
+
return Array.isArray(value)
|
|
373
|
+
? value.filter((item): item is string => typeof item === 'string') : [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
377
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
378
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { CURRENT_SCHEMA_VERSION } from '../db/migrations.js';
|
|
3
|
+
import type { CompactGraphV1, CompactSourceContext } from './014-compact-contract.js';
|
|
4
|
+
import { CompactObservationCollector } from './014-compact-contract.js';
|
|
5
|
+
import { projectCompactGraph } from './016-compact-projector.js';
|
|
6
|
+
import { traceWithObserver } from './trace-engine.js';
|
|
7
|
+
import type { TraceOptions, TraceResult, TraceStart } from '../types.js';
|
|
8
|
+
|
|
9
|
+
export interface CompactTraceExecution {
|
|
10
|
+
trace: TraceResult;
|
|
11
|
+
compact: CompactGraphV1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function compactTrace(
|
|
15
|
+
db: Db,
|
|
16
|
+
start: TraceStart,
|
|
17
|
+
options: TraceOptions,
|
|
18
|
+
): CompactGraphV1 {
|
|
19
|
+
return traceAndCompact(db, start, options).compact;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function traceAndCompact(
|
|
23
|
+
db: Db,
|
|
24
|
+
start: TraceStart,
|
|
25
|
+
options: TraceOptions,
|
|
26
|
+
): CompactTraceExecution {
|
|
27
|
+
const collector = new CompactObservationCollector();
|
|
28
|
+
const trace = traceWithObserver(db, start, options, collector);
|
|
29
|
+
const source = compactSourceContext(db, options, collector.workspaceId);
|
|
30
|
+
const compact = projectCompactGraph({
|
|
31
|
+
db, start, options, source, trace,
|
|
32
|
+
observations: collector.observations,
|
|
33
|
+
});
|
|
34
|
+
return { trace, compact };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compactSourceContext(
|
|
38
|
+
db: Db,
|
|
39
|
+
options: TraceOptions,
|
|
40
|
+
traversalWorkspaceId?: number,
|
|
41
|
+
): CompactSourceContext {
|
|
42
|
+
return {
|
|
43
|
+
schemaVersion: schemaVersion(db),
|
|
44
|
+
analyzerVersion: sourceAnalyzerVersion(
|
|
45
|
+
db, traversalWorkspaceId ?? options.workspaceId,
|
|
46
|
+
),
|
|
47
|
+
graphGeneration: graphGeneration(
|
|
48
|
+
db, traversalWorkspaceId ?? options.workspaceId,
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sourceAnalyzerVersion(
|
|
54
|
+
db: Db,
|
|
55
|
+
workspaceId: number | undefined,
|
|
56
|
+
): string {
|
|
57
|
+
const rows = db.prepare(`SELECT DISTINCT
|
|
58
|
+
COALESCE(fact_analyzer_version,'legacy_unknown') analyzerVersion
|
|
59
|
+
FROM repositories WHERE (? IS NULL OR workspace_id=?)
|
|
60
|
+
ORDER BY analyzerVersion COLLATE BINARY LIMIT 2`).all(
|
|
61
|
+
workspaceId, workspaceId,
|
|
62
|
+
);
|
|
63
|
+
if (rows.length === 0) return 'none';
|
|
64
|
+
if (rows.length > 1) return 'mixed';
|
|
65
|
+
return stringValue(rows[0]?.analyzerVersion) ?? 'legacy_unknown';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function graphGeneration(db: Db, workspaceId: number | undefined): number {
|
|
69
|
+
if (workspaceId === undefined) return 0;
|
|
70
|
+
const rows = db.prepare(`SELECT DISTINCT graph_generation generation
|
|
71
|
+
FROM repositories WHERE workspace_id=?
|
|
72
|
+
ORDER BY graph_generation LIMIT 2`).all(workspaceId);
|
|
73
|
+
return rows.length === 1 ? numberValue(rows[0]?.generation) ?? 0 : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function schemaVersion(db: Db): number {
|
|
77
|
+
const row = db.pragma('user_version')[0];
|
|
78
|
+
return numberValue(row?.user_version) ?? CURRENT_SCHEMA_VERSION;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function numberValue(value: unknown): number | undefined {
|
|
82
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stringValue(value: unknown): string | undefined {
|
|
86
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
87
|
+
}
|