@saptools/service-flow 0.1.72 → 0.1.73
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 +8 -0
- package/README.md +19 -6
- package/TECHNICAL-NOTE.md +11 -2
- package/dist/{chunk-Z6D433R5.js → chunk-32WOTGTS.js} +6714 -6161
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +189 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +90 -15
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/current-fact-semantics.ts +2 -26
- package/src/db/event-fact-semantics.ts +260 -61
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +4 -1
- package/src/db/package-target-invalidation.ts +4 -3
- package/src/db/schema.ts +1 -1
- package/src/indexer/repository-indexer.ts +50 -6
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +4 -2
- package/src/linker/event-shape-candidate-linker.ts +63 -20
- package/src/linker/event-subscription-handler-linker.ts +121 -30
- package/src/linker/package-event-constant-resolver.ts +22 -18
- package/src/output/repository-inspection.ts +11 -0
- package/src/parsers/environment-declarations.ts +104 -27
- package/src/parsers/event-call-analysis.ts +163 -35
- package/src/parsers/event-environment-reference.ts +18 -7
- package/src/parsers/event-receiver-analysis.ts +17 -27
- package/src/parsers/outbound-call-classifier.ts +1 -1
- package/src/parsers/stable-local-value.ts +12 -1
- package/src/parsers/string-constant-lookups.ts +78 -28
- package/src/trace/edge-target.ts +65 -0
- package/src/trace/event-subscriber-traversal.ts +71 -1
- package/src/trace/evidence.ts +0 -28
- package/src/trace/trace-scope-execution.ts +1 -1
- package/src/types.ts +3 -1
- package/src/version.ts +1 -1
- package/dist/chunk-Z6D433R5.js.map +0 -1
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
type SymbolImportBinding,
|
|
9
9
|
} from './symbol-import-bindings.js';
|
|
10
10
|
import type { RepositorySourceContext } from './ts-project.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_EVENT_ENVIRONMENT_KEYS,
|
|
13
|
+
normalizeEventEnvironmentKeys,
|
|
14
|
+
} from
|
|
12
15
|
'./environment-declarations.js';
|
|
13
16
|
|
|
14
17
|
export type EventEnvironmentTransform = 'toUpperCase' | 'toLowerCase';
|
|
@@ -34,13 +37,10 @@ interface ResolutionContext {
|
|
|
34
37
|
sourceFile: string;
|
|
35
38
|
depth: number;
|
|
36
39
|
seen: Set<string>;
|
|
40
|
+
allowedKeys: ReadonlySet<string>;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const maxEnvironmentReferenceDepth = 6;
|
|
40
|
-
const allowedEnvironmentKeys = new Set<string>(
|
|
41
|
-
EVENT_ENVIRONMENT_KEY_ALLOWLIST,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
44
|
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
45
45
|
if (ts.isParenthesizedExpression(expression)
|
|
46
46
|
|| ts.isAsExpression(expression)
|
|
@@ -173,6 +173,15 @@ function identifierReference(
|
|
|
173
173
|
const local = resolveBinding(identifier, identifier);
|
|
174
174
|
if (local.declaration && local.initializer && local.immutable)
|
|
175
175
|
return resolveEnvironmentReference(local.initializer, context);
|
|
176
|
+
const binding = bindingFor(
|
|
177
|
+
identifier, collectSymbolImportBindings(context.source),
|
|
178
|
+
);
|
|
179
|
+
if (binding?.moduleKind === 'package') return {
|
|
180
|
+
status: 'refused',
|
|
181
|
+
sourceKey: identifier.getText(context.source),
|
|
182
|
+
transforms: [],
|
|
183
|
+
reason: 'event_environment_package_import_unsupported',
|
|
184
|
+
};
|
|
176
185
|
const imported = importedIdentifier(identifier, context);
|
|
177
186
|
if (!imported) return undefined;
|
|
178
187
|
return resolveEnvironmentReference(imported.initializer, {
|
|
@@ -199,7 +208,7 @@ function resolveEnvironmentReference(
|
|
|
199
208
|
const next = { ...context, depth: context.depth + 1, seen };
|
|
200
209
|
const value = unwrapExpression(expression);
|
|
201
210
|
const key = processEnvironmentKey(value);
|
|
202
|
-
if (key &&
|
|
211
|
+
if (key && context.allowedKeys.has(key)) return {
|
|
203
212
|
status: 'resolved', sourceKey, environmentKey: key, transforms: [],
|
|
204
213
|
sourceFile: context.sourceFile,
|
|
205
214
|
startOffset: value.getStart(context.source), endOffset: value.getEnd(),
|
|
@@ -215,9 +224,11 @@ export function createEventEnvironmentReferenceResolver(
|
|
|
215
224
|
sources: RepositorySourceContext,
|
|
216
225
|
source: ts.SourceFile,
|
|
217
226
|
sourceFile: string,
|
|
227
|
+
configuredKeys: readonly string[] = DEFAULT_EVENT_ENVIRONMENT_KEYS,
|
|
218
228
|
): EventEnvironmentReferenceResolver {
|
|
229
|
+
const allowedKeys = new Set(normalizeEventEnvironmentKeys(configuredKeys));
|
|
219
230
|
return (expression) => resolveEnvironmentReference(expression, {
|
|
220
|
-
sources, source, sourceFile, depth: 0, seen: new Set(),
|
|
231
|
+
sources, source, sourceFile, depth: 0, seen: new Set(), allowedKeys,
|
|
221
232
|
});
|
|
222
233
|
}
|
|
223
234
|
|
|
@@ -23,7 +23,7 @@ export type EventReceiverUnresolvedReason =
|
|
|
23
23
|
export interface EventReceiverEvidenceSite {
|
|
24
24
|
startOffset: number;
|
|
25
25
|
endOffset: number;
|
|
26
|
-
flow: BindingLexicalSite['flow'];
|
|
26
|
+
flow: BindingLexicalSite['flow'] | 'reference';
|
|
27
27
|
connect: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -34,6 +34,7 @@ export interface EventReceiverProof {
|
|
|
34
34
|
receiverClassification: 'cap_evidence' | 'name_fallback' | 'unproven';
|
|
35
35
|
receiverProof: string;
|
|
36
36
|
unresolvedReason?: EventReceiverUnresolvedReason;
|
|
37
|
+
fallbackRefusedReason?: string;
|
|
37
38
|
consideredBindingSites: EventReceiverEvidenceSite[];
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -41,7 +42,6 @@ export interface EventReceiverIndex {
|
|
|
41
42
|
source: ts.SourceFile;
|
|
42
43
|
lexical: BindingLexicalIndex;
|
|
43
44
|
imports: SymbolImportBinding[];
|
|
44
|
-
compatibilityNames: Set<string>;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const eventReceiverNames = new Set([
|
|
@@ -218,17 +218,6 @@ function sameExecutionScope(
|
|
|
218
218
|
return sameScope(left, right);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function unsupportedBranch(node: ts.Node): boolean {
|
|
222
|
-
let current: ts.Node | undefined = node.parent;
|
|
223
|
-
while (current && !ts.isSourceFile(current) && !ts.isFunctionLike(current)) {
|
|
224
|
-
if (ts.isIfStatement(current) || ts.isConditionalExpression(current)
|
|
225
|
-
|| ts.isSwitchStatement(current)
|
|
226
|
-
|| ts.isIterationStatement(current, false)) return true;
|
|
227
|
-
current = current.parent;
|
|
228
|
-
}
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
221
|
function reachingSites(
|
|
233
222
|
index: EventReceiverIndex,
|
|
234
223
|
declaration: BindingLexicalSite,
|
|
@@ -275,11 +264,6 @@ function lexicalProof(
|
|
|
275
264
|
return value ? [value] : [];
|
|
276
265
|
});
|
|
277
266
|
const considered = evidenceSites(sites, identifier.text, index.imports);
|
|
278
|
-
if (sites.some((site) => unsupportedBranch(site.node)))
|
|
279
|
-
return unproven(
|
|
280
|
-
'event_receiver_unproven_binding', 'branch_dependent_assignment',
|
|
281
|
-
considered,
|
|
282
|
-
);
|
|
283
267
|
if (expressions.length > 0
|
|
284
268
|
&& expressions.every((value) => isCapConnect(value, index.imports)))
|
|
285
269
|
return {
|
|
@@ -313,15 +297,24 @@ function unproven(
|
|
|
313
297
|
}
|
|
314
298
|
|
|
315
299
|
function compatibilityFallback(
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
identifier: ts.Identifier,
|
|
301
|
+
refusedReason: string,
|
|
302
|
+
consideredBindingSites: EventReceiverEvidenceSite[],
|
|
318
303
|
): Omit<EventReceiverProof, 'effectiveReceiver' | 'receiver'
|
|
319
304
|
| 'rootReceiver'> | undefined {
|
|
320
|
-
return eventReceiverNames.has(
|
|
305
|
+
return eventReceiverNames.has(identifier.text)
|
|
321
306
|
? {
|
|
322
307
|
receiverClassification: 'name_fallback',
|
|
323
308
|
receiverProof: 'compatibility_name_fallback',
|
|
324
|
-
|
|
309
|
+
fallbackRefusedReason: refusedReason,
|
|
310
|
+
consideredBindingSites: consideredBindingSites.length > 0
|
|
311
|
+
? consideredBindingSites
|
|
312
|
+
: [{
|
|
313
|
+
startOffset: identifier.getStart(identifier.getSourceFile()),
|
|
314
|
+
endOffset: identifier.getEnd(),
|
|
315
|
+
flow: 'reference',
|
|
316
|
+
connect: false,
|
|
317
|
+
}],
|
|
325
318
|
}
|
|
326
319
|
: undefined;
|
|
327
320
|
}
|
|
@@ -344,9 +337,8 @@ function identifierProof(
|
|
|
344
337
|
consideredBindingSites: [],
|
|
345
338
|
};
|
|
346
339
|
const proven = lexicalProof(identifier, use, index);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
?? proven
|
|
340
|
+
return proven
|
|
341
|
+
?? compatibilityFallback(identifier, 'binding_not_found', [])
|
|
350
342
|
?? unproven('event_receiver_unproven_binding', 'binding_not_found');
|
|
351
343
|
}
|
|
352
344
|
|
|
@@ -364,13 +356,11 @@ function rootIdentifier(
|
|
|
364
356
|
|
|
365
357
|
export function createEventReceiverIndex(
|
|
366
358
|
source: ts.SourceFile,
|
|
367
|
-
compatibilityNames: Set<string>,
|
|
368
359
|
): EventReceiverIndex {
|
|
369
360
|
return {
|
|
370
361
|
source,
|
|
371
362
|
lexical: createBindingLexicalIndex(source),
|
|
372
363
|
imports: collectSymbolImportBindings(source),
|
|
373
|
-
compatibilityNames,
|
|
374
364
|
};
|
|
375
365
|
}
|
|
376
366
|
|
|
@@ -566,7 +566,7 @@ export function classifyOutboundCallsInSource(
|
|
|
566
566
|
const initializers = variableInitializers(source);
|
|
567
567
|
const serviceVariables = collectServiceVariables(source);
|
|
568
568
|
const eventAnalysis = createEventCallAnalysisContext(
|
|
569
|
-
source,
|
|
569
|
+
source, options.importedEventNameResolver,
|
|
570
570
|
options.eventEnvironmentReferenceResolver,
|
|
571
571
|
);
|
|
572
572
|
const wrapperSpecs = collectWrapperSpecs(source);
|
|
@@ -205,6 +205,16 @@ function inertUnaryUse(node: ts.Node): boolean {
|
|
|
205
205
|
|| ts.isTypeOfExpression(parent)) && parent.expression === node));
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
function typePositionUse(node: ts.Node): boolean {
|
|
209
|
+
let current: ts.Node | undefined = node.parent;
|
|
210
|
+
while (current && !ts.isStatement(current)
|
|
211
|
+
&& !ts.isSourceFile(current)) {
|
|
212
|
+
if (ts.isTypeNode(current)) return true;
|
|
213
|
+
current = current.parent;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
function safeReferenceUse(
|
|
209
219
|
node: ts.Identifier,
|
|
210
220
|
declaration: ts.Identifier,
|
|
@@ -213,7 +223,8 @@ function safeReferenceUse(
|
|
|
213
223
|
return memberReceiverUse(use, declaration)
|
|
214
224
|
|| commonJsExportUse(use)
|
|
215
225
|
|| esmExportUse(use)
|
|
216
|
-
|| inertUnaryUse(use)
|
|
226
|
+
|| inertUnaryUse(use)
|
|
227
|
+
|| typePositionUse(use);
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
export function stableLocalValueReference(
|
|
@@ -33,7 +33,9 @@ export interface StaticStringRefusal {
|
|
|
33
33
|
exported: boolean;
|
|
34
34
|
stable: boolean;
|
|
35
35
|
reason: 'event_name_constant_member_not_string'
|
|
36
|
-
| 'event_name_constant_container_mutable'
|
|
36
|
+
| 'event_name_constant_container_mutable'
|
|
37
|
+
| 'event_name_constant_container_unsafe_reference'
|
|
38
|
+
| 'event_name_constant_container_unsupported_shape';
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface StringConstantLookups {
|
|
@@ -144,7 +146,7 @@ function collectEnum(
|
|
|
144
146
|
source, key, member, {
|
|
145
147
|
kind: 'enum_member', containerName: statement.name.text,
|
|
146
148
|
memberName, exported, stable,
|
|
147
|
-
reason: '
|
|
149
|
+
reason: 'event_name_constant_container_unsafe_reference',
|
|
148
150
|
},
|
|
149
151
|
));
|
|
150
152
|
else if (!value) lookups.refusedMembers.set(key, refusalFact(
|
|
@@ -173,6 +175,44 @@ function objectLiteral(
|
|
|
173
175
|
return ts.isObjectLiteralExpression(value) ? value : undefined;
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
function objectMemberName(
|
|
179
|
+
property: ts.ObjectLiteralElementLike,
|
|
180
|
+
): string | undefined {
|
|
181
|
+
if (ts.isSpreadAssignment(property)) return undefined;
|
|
182
|
+
return property.name ? propertyName(property.name) : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function supportedObjectShape(object: ts.ObjectLiteralExpression): boolean {
|
|
186
|
+
return object.properties.every((property) =>
|
|
187
|
+
ts.isPropertyAssignment(property)
|
|
188
|
+
&& propertyName(property.name) !== undefined);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function refuseObjectShape(
|
|
192
|
+
source: ts.SourceFile,
|
|
193
|
+
object: ts.ObjectLiteralExpression,
|
|
194
|
+
declaration: ts.VariableDeclaration,
|
|
195
|
+
exported: boolean,
|
|
196
|
+
lookups: StringConstantLookups,
|
|
197
|
+
): void {
|
|
198
|
+
if (!ts.isIdentifier(declaration.name)) return;
|
|
199
|
+
for (const property of object.properties) {
|
|
200
|
+
const memberName = objectMemberName(property);
|
|
201
|
+
if (!memberName) continue;
|
|
202
|
+
const key = `${declaration.name.text}.${memberName}`;
|
|
203
|
+
lookups.refusedMembers.set(key, refusalFact(
|
|
204
|
+
source, key, property, {
|
|
205
|
+
kind: 'const_object_property',
|
|
206
|
+
containerName: declaration.name.text,
|
|
207
|
+
memberName,
|
|
208
|
+
exported,
|
|
209
|
+
stable: false,
|
|
210
|
+
reason: 'event_name_constant_container_unsupported_shape',
|
|
211
|
+
},
|
|
212
|
+
));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
176
216
|
function collectObject(
|
|
177
217
|
source: ts.SourceFile,
|
|
178
218
|
statement: ts.VariableStatement,
|
|
@@ -186,38 +226,48 @@ function collectObject(
|
|
|
186
226
|
const stable = stableLocalValueReference(source, declaration.name);
|
|
187
227
|
const exported = hasExportModifier(statement)
|
|
188
228
|
|| exports.has(declaration.name.text);
|
|
229
|
+
if (!supportedObjectShape(object)) {
|
|
230
|
+
refuseObjectShape(source, object, declaration, exported, lookups);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
189
233
|
for (const property of object.properties) {
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
if (ts.isPropertyAssignment(property))
|
|
235
|
+
collectObjectProperty(
|
|
236
|
+
source, property, declaration.name.text, exported, stable, lookups,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function collectObjectProperty(
|
|
242
|
+
source: ts.SourceFile,
|
|
243
|
+
property: ts.PropertyAssignment,
|
|
244
|
+
containerName: string,
|
|
245
|
+
exported: boolean,
|
|
246
|
+
stable: boolean,
|
|
247
|
+
lookups: StringConstantLookups,
|
|
248
|
+
): void {
|
|
249
|
+
const memberName = propertyName(property.name);
|
|
250
|
+
if (!memberName) return;
|
|
251
|
+
const key = `${containerName}.${memberName}`;
|
|
252
|
+
const value = stringValue(property.initializer);
|
|
253
|
+
if (!stable || !value) {
|
|
254
|
+
lookups.refusedMembers.set(key, refusalFact(
|
|
204
255
|
source, key, property, {
|
|
205
|
-
kind: 'const_object_property',
|
|
206
|
-
containerName: declaration.name.text, memberName,
|
|
256
|
+
kind: 'const_object_property', containerName, memberName,
|
|
207
257
|
exported, stable,
|
|
208
|
-
reason:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
else lookups.objectProperties.set(key, staticFact(
|
|
212
|
-
source, key, value, property, {
|
|
213
|
-
kind: 'const_object_property',
|
|
214
|
-
containerName: declaration.name.text,
|
|
215
|
-
memberName,
|
|
216
|
-
exported,
|
|
217
|
-
stable,
|
|
258
|
+
reason: stable
|
|
259
|
+
? 'event_name_constant_member_not_string'
|
|
260
|
+
: 'event_name_constant_container_unsafe_reference',
|
|
218
261
|
},
|
|
219
262
|
));
|
|
263
|
+
return;
|
|
220
264
|
}
|
|
265
|
+
lookups.objectProperties.set(key, staticFact(
|
|
266
|
+
source, key, value, property, {
|
|
267
|
+
kind: 'const_object_property', containerName, memberName,
|
|
268
|
+
exported, stable,
|
|
269
|
+
},
|
|
270
|
+
));
|
|
221
271
|
}
|
|
222
272
|
|
|
223
273
|
function collectIdentifier(
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TraceGraphRow } from './evidence.js';
|
|
2
|
+
|
|
3
|
+
function record(value: unknown): Record<string, unknown> {
|
|
4
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
5
|
+
? value as Record<string, unknown> : {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function text(value: unknown): string | undefined {
|
|
9
|
+
return typeof value === 'string' ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function effectiveTarget(
|
|
13
|
+
evidence: Record<string, unknown>,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const effective = record(evidence.effectiveResolution);
|
|
16
|
+
const service = text(
|
|
17
|
+
effective.targetServicePath ?? evidence.targetServicePath,
|
|
18
|
+
);
|
|
19
|
+
const operation = text(
|
|
20
|
+
effective.targetOperationPath ?? evidence.targetOperationPath,
|
|
21
|
+
);
|
|
22
|
+
return service && operation ? `${service}${operation}` : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runtimeCandidateTarget(
|
|
26
|
+
evidence: Record<string, unknown>,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
const candidate = record(evidence.runtimeResolvedCandidate);
|
|
29
|
+
const service = text(candidate.servicePath);
|
|
30
|
+
const operation = text(candidate.operationPath);
|
|
31
|
+
return service && operation ? `${service}${operation}` : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function semanticEdgeTarget(
|
|
35
|
+
row: TraceGraphRow,
|
|
36
|
+
evidence: Record<string, unknown>,
|
|
37
|
+
): string | undefined {
|
|
38
|
+
if (row.edge_type === 'HANDLER_RUNS_DB_QUERY')
|
|
39
|
+
return `Entity: ${row.to_id || 'unknown'}`;
|
|
40
|
+
if (row.edge_type === 'HANDLER_RUNS_REMOTE_QUERY')
|
|
41
|
+
return text(evidence.remoteQueryTarget)
|
|
42
|
+
?? `Remote query: ${row.to_id || 'unknown'}`;
|
|
43
|
+
if (row.edge_type === 'HANDLER_CALLS_EXTERNAL_HTTP') {
|
|
44
|
+
const target = record(evidence.externalTarget);
|
|
45
|
+
return text(target.label) ?? `External endpoint: ${row.to_id || 'unknown'}`;
|
|
46
|
+
}
|
|
47
|
+
return row.edge_type === 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER'
|
|
48
|
+
? text(evidence.eventShapeCandidateTargetLabel) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function edgeTarget(
|
|
52
|
+
row: TraceGraphRow,
|
|
53
|
+
evidence: Record<string, unknown>,
|
|
54
|
+
): string {
|
|
55
|
+
const exact = effectiveTarget(evidence) ?? runtimeCandidateTarget(evidence);
|
|
56
|
+
if (exact) return exact;
|
|
57
|
+
const semantic = semanticEdgeTarget(row, evidence);
|
|
58
|
+
if (semantic) return semantic;
|
|
59
|
+
const service = text(evidence.servicePath);
|
|
60
|
+
const operation = text(evidence.operationPath);
|
|
61
|
+
if (service && operation) return `${service}${operation}`;
|
|
62
|
+
const targetOperation = text(evidence.targetOperation);
|
|
63
|
+
const targetRepo = text(evidence.targetRepo) ?? '';
|
|
64
|
+
return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
65
|
+
}
|
|
@@ -64,9 +64,23 @@ export interface EventSubscriberTransition {
|
|
|
64
64
|
omittedSymbolCallUnresolvedReasonCharacterCount?: number;
|
|
65
65
|
matchStrategy?: string;
|
|
66
66
|
dispatchCertainty?: string;
|
|
67
|
+
subscriptionConsumerRepoId?: number;
|
|
68
|
+
subscriptionConsumerRepoName?: string;
|
|
69
|
+
dispatchProvenances?: EventDispatchProvenance[];
|
|
70
|
+
dispatchProvenanceCount?: number;
|
|
71
|
+
shownDispatchProvenanceCount?: number;
|
|
72
|
+
omittedDispatchProvenanceCount?: number;
|
|
67
73
|
handler?: EventSubscriberSymbolTarget;
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
export interface EventDispatchProvenance {
|
|
77
|
+
graphEdgeId: number;
|
|
78
|
+
matchStrategy: string;
|
|
79
|
+
dispatchCertainty: string;
|
|
80
|
+
consumerRepoId?: number;
|
|
81
|
+
consumerRepoName?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
export interface EventSubscriberTransitionQuery {
|
|
71
85
|
workspaceId: number;
|
|
72
86
|
graphGeneration: number;
|
|
@@ -171,6 +185,10 @@ export function eventTransitionEvidence(
|
|
|
171
185
|
eventName: transition.eventName,
|
|
172
186
|
matchStrategy: transition.matchStrategy ?? 'workspace_exact_event_name',
|
|
173
187
|
dispatchCertainty: transition.dispatchCertainty ?? 'static_name_only',
|
|
188
|
+
dispatchProvenances: transition.dispatchProvenances,
|
|
189
|
+
dispatchProvenanceCount: transition.dispatchProvenanceCount,
|
|
190
|
+
shownDispatchProvenanceCount: transition.shownDispatchProvenanceCount,
|
|
191
|
+
omittedDispatchProvenanceCount: transition.omittedDispatchProvenanceCount,
|
|
174
192
|
associationBasis: transition.associationBasis,
|
|
175
193
|
dispatchScope: transition.dispatchScope,
|
|
176
194
|
roleSiteMatchCount: transition.roleSiteMatchCount,
|
|
@@ -239,11 +257,57 @@ export function loadEventSubscriberTransitions(
|
|
|
239
257
|
subscribe.call_site_start_offset,subscribe.call_site_end_offset,ge.id`).all(
|
|
240
258
|
query.workspaceId, query.graphGeneration,
|
|
241
259
|
);
|
|
242
|
-
|
|
260
|
+
const transitions = rows.flatMap((raw) => {
|
|
243
261
|
const row = eventRowForQuery(raw, query);
|
|
244
262
|
const transition = row ? transitionFromRow(row) : undefined;
|
|
245
263
|
return transition ? [transition] : [];
|
|
246
264
|
});
|
|
265
|
+
return deduplicateDispatchProvenance(transitions);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const dispatchProvenanceLimit = 5;
|
|
269
|
+
|
|
270
|
+
function transitionIdentity(value: EventSubscriberTransition): string {
|
|
271
|
+
const subscription = value.subscribeCallId ?? `edge:${value.graphEdgeId}`;
|
|
272
|
+
return `${value.eventName}\0${subscription}\0${value.targetKind}\0${
|
|
273
|
+
value.targetId}\0${value.status}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function dispatchProvenance(
|
|
277
|
+
value: EventSubscriberTransition,
|
|
278
|
+
): EventDispatchProvenance {
|
|
279
|
+
return {
|
|
280
|
+
graphEdgeId: value.graphEdgeId,
|
|
281
|
+
matchStrategy: value.matchStrategy ?? 'workspace_exact_event_name',
|
|
282
|
+
dispatchCertainty: value.dispatchCertainty ?? 'static_name_only',
|
|
283
|
+
consumerRepoId: value.subscriptionConsumerRepoId,
|
|
284
|
+
consumerRepoName: value.subscriptionConsumerRepoName,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function deduplicateDispatchProvenance(
|
|
289
|
+
values: EventSubscriberTransition[],
|
|
290
|
+
): EventSubscriberTransition[] {
|
|
291
|
+
const groups = new Map<string, EventSubscriberTransition[]>();
|
|
292
|
+
for (const value of values) {
|
|
293
|
+
const key = transitionIdentity(value);
|
|
294
|
+
groups.set(key, [...(groups.get(key) ?? []), value]);
|
|
295
|
+
}
|
|
296
|
+
return [...groups.values()].map((group) => {
|
|
297
|
+
const first = group[0];
|
|
298
|
+
if (!first || group.length === 1) return first;
|
|
299
|
+
const provenances = group.map(dispatchProvenance).slice(
|
|
300
|
+
0, dispatchProvenanceLimit,
|
|
301
|
+
);
|
|
302
|
+
return {
|
|
303
|
+
...first,
|
|
304
|
+
dispatchProvenances: provenances,
|
|
305
|
+
dispatchProvenanceCount: group.length,
|
|
306
|
+
shownDispatchProvenanceCount: provenances.length,
|
|
307
|
+
omittedDispatchProvenanceCount: group.length - provenances.length,
|
|
308
|
+
};
|
|
309
|
+
}).filter((value): value is EventSubscriberTransition =>
|
|
310
|
+
value !== undefined);
|
|
247
311
|
}
|
|
248
312
|
|
|
249
313
|
function eventRowForQuery(
|
|
@@ -394,6 +458,12 @@ function associationEvidence(
|
|
|
394
458
|
: undefined,
|
|
395
459
|
matchStrategy: stringValue(evidence.matchStrategy),
|
|
396
460
|
dispatchCertainty: stringValue(evidence.dispatchCertainty),
|
|
461
|
+
subscriptionConsumerRepoId: numberValue(
|
|
462
|
+
evidence.subscriptionConsumerRepositoryId,
|
|
463
|
+
),
|
|
464
|
+
subscriptionConsumerRepoName: stringValue(
|
|
465
|
+
evidence.subscriptionConsumerRepositoryName,
|
|
466
|
+
),
|
|
397
467
|
};
|
|
398
468
|
}
|
|
399
469
|
|
package/src/trace/evidence.ts
CHANGED
|
@@ -23,13 +23,6 @@ export interface TraceGraphRow extends Record<string, unknown> {
|
|
|
23
23
|
status?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
interface Candidate {
|
|
27
|
-
servicePath?: string;
|
|
28
|
-
operationPath?: string;
|
|
29
|
-
repoName?: string;
|
|
30
|
-
operationName?: string;
|
|
31
|
-
score?: number;
|
|
32
|
-
}
|
|
33
26
|
interface RuntimeDiagnosticTotals {
|
|
34
27
|
missing: Set<string>;
|
|
35
28
|
candidateCount: number;
|
|
@@ -342,27 +335,6 @@ export function runtimeNoCandidateDiagnostics(
|
|
|
342
335
|
});
|
|
343
336
|
}
|
|
344
337
|
|
|
345
|
-
export function edgeTarget(row: TraceGraphRow, evidence: Record<string, unknown>): string {
|
|
346
|
-
const effective = parseObject(evidence.effectiveResolution);
|
|
347
|
-
const targetServicePath = stringValue(effective.targetServicePath ?? evidence.targetServicePath);
|
|
348
|
-
const targetOperationPath = stringValue(effective.targetOperationPath ?? evidence.targetOperationPath);
|
|
349
|
-
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
350
|
-
const runtimeCandidate = evidence.runtimeResolvedCandidate as Candidate | undefined;
|
|
351
|
-
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
352
|
-
const servicePath = stringValue(evidence.servicePath);
|
|
353
|
-
const operationPath = stringValue(evidence.operationPath);
|
|
354
|
-
const targetOperation = stringValue(evidence.targetOperation);
|
|
355
|
-
const targetRepo = stringValue(evidence.targetRepo) ?? '';
|
|
356
|
-
if (row.edge_type === 'HANDLER_RUNS_DB_QUERY') return `Entity: ${row.to_id || 'unknown'}`;
|
|
357
|
-
if (row.edge_type === 'HANDLER_RUNS_REMOTE_QUERY') return stringValue(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || 'unknown'}`;
|
|
358
|
-
if (row.edge_type === 'HANDLER_CALLS_EXTERNAL_HTTP') {
|
|
359
|
-
const target = parseObject(evidence.externalTarget);
|
|
360
|
-
return stringValue(target.label) ?? `External endpoint: ${row.to_id || 'unknown'}`;
|
|
361
|
-
}
|
|
362
|
-
if (servicePath && operationPath) return `${servicePath}${operationPath}`;
|
|
363
|
-
return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
338
|
function persistedResolution(row: TraceGraphRow): Record<string, unknown> {
|
|
367
339
|
return {
|
|
368
340
|
status: row.status,
|
|
@@ -2,10 +2,10 @@ import type { Db } from '../db/connection.js';
|
|
|
2
2
|
import type { TraceEdge, TraceOptions } from '../types.js';
|
|
3
3
|
import {
|
|
4
4
|
baseTraceEvidence,
|
|
5
|
-
edgeTarget,
|
|
6
5
|
runtimeResolution,
|
|
7
6
|
type TraceGraphRow,
|
|
8
7
|
} from './evidence.js';
|
|
8
|
+
import { edgeTarget } from './edge-target.js';
|
|
9
9
|
import { dynamicCandidateBranches } from './dynamic-branches.js';
|
|
10
10
|
import {
|
|
11
11
|
contextualRuntimeResolution,
|
package/src/types.ts
CHANGED
|
@@ -286,7 +286,9 @@ export interface GeneratedConstantFact {
|
|
|
286
286
|
stable: boolean;
|
|
287
287
|
resolutionStatus: 'resolved' | 'refused';
|
|
288
288
|
unresolvedReason?: 'event_name_constant_member_not_string'
|
|
289
|
-
| 'event_name_constant_container_mutable'
|
|
289
|
+
| 'event_name_constant_container_mutable'
|
|
290
|
+
| 'event_name_constant_container_unsafe_reference'
|
|
291
|
+
| 'event_name_constant_container_unsupported_shape';
|
|
290
292
|
declarationStartOffset: number;
|
|
291
293
|
declarationEndOffset: number;
|
|
292
294
|
valueStartOffset: number;
|
package/src/version.ts
CHANGED