@saptools/service-flow 0.1.70 → 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 +21 -0
- package/README.md +23 -6
- package/TECHNICAL-NOTE.md +24 -2
- package/dist/{chunk-GSLFY6J2.js → chunk-32WOTGTS.js} +12061 -9288
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +854 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -14
- package/dist/index.js +2 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +446 -0
- package/src/cli/doctor.ts +4 -6
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -29
- package/src/db/event-fact-semantics.ts +546 -0
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +15 -1
- package/src/db/package-target-invalidation.ts +80 -0
- package/src/db/repositories.ts +28 -2
- package/src/db/schema-structure.ts +41 -1
- package/src/db/schema.ts +6 -2
- package/src/indexer/repository-indexer.ts +93 -10
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +27 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +204 -0
- package/src/linker/event-subscription-handler-linker.ts +220 -25
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +302 -0
- package/src/output/repository-inspection.ts +11 -0
- package/src/output/table-output.ts +13 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +404 -0
- package/src/parsers/event-call-analysis.ts +370 -0
- package/src/parsers/event-environment-reference.ts +242 -0
- package/src/parsers/event-loop-registration.ts +132 -0
- package/src/parsers/event-name-import-resolution.ts +243 -0
- package/src/parsers/event-receiver-analysis.ts +394 -0
- package/src/parsers/event-subscription-facts.ts +4 -0
- package/src/parsers/generated-constants-parser.ts +80 -14
- package/src/parsers/outbound-call-classifier.ts +27 -124
- package/src/parsers/outbound-call-parser.ts +13 -1
- package/src/parsers/outbound-expression-analysis.ts +2 -2
- package/src/parsers/stable-local-value.ts +42 -10
- package/src/parsers/string-constant-lookups.ts +408 -0
- package/src/trace/edge-target.ts +65 -0
- package/src/trace/event-runtime-resolution.ts +24 -3
- package/src/trace/event-shape-candidate-trace.ts +172 -0
- package/src/trace/event-subscriber-traversal.ts +90 -8
- package/src/trace/evidence.ts +7 -28
- package/src/trace/trace-scope-execution.ts +22 -30
- package/src/types.ts +19 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { stableLocalValueReference } from './stable-local-value.js';
|
|
3
|
+
import { lexicalIdentifierDeclaration } from './symbol-import-bindings.js';
|
|
4
|
+
|
|
5
|
+
export type StaticStringConstantKind =
|
|
6
|
+
| 'const_identifier'
|
|
7
|
+
| 'enum_member'
|
|
8
|
+
| 'const_object_property';
|
|
9
|
+
|
|
10
|
+
export interface StaticStringConstant {
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
kind: StaticStringConstantKind;
|
|
14
|
+
containerName?: string;
|
|
15
|
+
memberName?: string;
|
|
16
|
+
sourceFile: string;
|
|
17
|
+
declarationStartOffset: number;
|
|
18
|
+
declarationEndOffset: number;
|
|
19
|
+
valueStartOffset: number;
|
|
20
|
+
valueEndOffset: number;
|
|
21
|
+
exported: boolean;
|
|
22
|
+
stable: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StaticStringRefusal {
|
|
26
|
+
key: string;
|
|
27
|
+
kind: StaticStringConstantKind;
|
|
28
|
+
containerName?: string;
|
|
29
|
+
memberName?: string;
|
|
30
|
+
sourceFile: string;
|
|
31
|
+
declarationStartOffset: number;
|
|
32
|
+
declarationEndOffset: number;
|
|
33
|
+
exported: boolean;
|
|
34
|
+
stable: boolean;
|
|
35
|
+
reason: 'event_name_constant_member_not_string'
|
|
36
|
+
| 'event_name_constant_container_mutable'
|
|
37
|
+
| 'event_name_constant_container_unsafe_reference'
|
|
38
|
+
| 'event_name_constant_container_unsupported_shape';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StringConstantLookups {
|
|
42
|
+
identifiers: Map<string, StaticStringConstant>;
|
|
43
|
+
enumMembers: Map<string, StaticStringConstant>;
|
|
44
|
+
objectProperties: Map<string, StaticStringConstant>;
|
|
45
|
+
refusedMembers: Map<string, StaticStringRefusal>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type StaticStringLookupResult =
|
|
49
|
+
| { status: 'resolved'; constant: StaticStringConstant }
|
|
50
|
+
| { status: 'refused'; reason: string }
|
|
51
|
+
| { status: 'not_found' };
|
|
52
|
+
|
|
53
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
54
|
+
if (ts.isParenthesizedExpression(expression)
|
|
55
|
+
|| ts.isAsExpression(expression)
|
|
56
|
+
|| ts.isSatisfiesExpression(expression)
|
|
57
|
+
|| ts.isTypeAssertionExpression(expression))
|
|
58
|
+
return unwrapExpression(expression.expression);
|
|
59
|
+
return expression;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stringValue(
|
|
63
|
+
expression: ts.Expression | undefined,
|
|
64
|
+
): { value: string; node: ts.Expression } | undefined {
|
|
65
|
+
if (!expression) return undefined;
|
|
66
|
+
const value = unwrapExpression(expression);
|
|
67
|
+
return ts.isStringLiteralLike(value)
|
|
68
|
+
? { value: value.text, node: value } : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function propertyName(name: ts.PropertyName): string | undefined {
|
|
72
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
73
|
+
|| ts.isNumericLiteral(name) ? name.text : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function exportedNames(source: ts.SourceFile): Set<string> {
|
|
77
|
+
const names = new Set<string>();
|
|
78
|
+
for (const statement of source.statements) {
|
|
79
|
+
if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier
|
|
80
|
+
|| !statement.exportClause
|
|
81
|
+
|| !ts.isNamedExports(statement.exportClause)) continue;
|
|
82
|
+
for (const element of statement.exportClause.elements)
|
|
83
|
+
names.add(element.propertyName?.text ?? element.name.text);
|
|
84
|
+
}
|
|
85
|
+
return names;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasExportModifier(node: ts.Node): boolean {
|
|
89
|
+
return ts.canHaveModifiers(node)
|
|
90
|
+
&& Boolean(ts.getModifiers(node)?.some((modifier) =>
|
|
91
|
+
modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function staticFact(
|
|
95
|
+
source: ts.SourceFile,
|
|
96
|
+
key: string,
|
|
97
|
+
value: { value: string; node: ts.Expression },
|
|
98
|
+
declaration: ts.Node,
|
|
99
|
+
fields: Pick<StaticStringConstant, 'kind' | 'exported' | 'stable'>
|
|
100
|
+
& Partial<Pick<StaticStringConstant, 'containerName' | 'memberName'>>,
|
|
101
|
+
): StaticStringConstant {
|
|
102
|
+
return {
|
|
103
|
+
key,
|
|
104
|
+
value: value.value,
|
|
105
|
+
sourceFile: source.fileName,
|
|
106
|
+
declarationStartOffset: declaration.getStart(source),
|
|
107
|
+
declarationEndOffset: declaration.getEnd(),
|
|
108
|
+
valueStartOffset: value.node.getStart(source),
|
|
109
|
+
valueEndOffset: value.node.getEnd(),
|
|
110
|
+
...fields,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function refusalFact(
|
|
115
|
+
source: ts.SourceFile,
|
|
116
|
+
key: string,
|
|
117
|
+
declaration: ts.Node,
|
|
118
|
+
fields: Pick<StaticStringRefusal,
|
|
119
|
+
'kind' | 'exported' | 'stable' | 'reason'>
|
|
120
|
+
& Partial<Pick<StaticStringRefusal, 'containerName' | 'memberName'>>,
|
|
121
|
+
): StaticStringRefusal {
|
|
122
|
+
return {
|
|
123
|
+
key,
|
|
124
|
+
sourceFile: source.fileName,
|
|
125
|
+
declarationStartOffset: declaration.getStart(source),
|
|
126
|
+
declarationEndOffset: declaration.getEnd(),
|
|
127
|
+
...fields,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function collectEnum(
|
|
132
|
+
source: ts.SourceFile,
|
|
133
|
+
statement: ts.EnumDeclaration,
|
|
134
|
+
exports: Set<string>,
|
|
135
|
+
lookups: StringConstantLookups,
|
|
136
|
+
): void {
|
|
137
|
+
const stable = stableLocalValueReference(source, statement.name);
|
|
138
|
+
const exported = hasExportModifier(statement)
|
|
139
|
+
|| exports.has(statement.name.text);
|
|
140
|
+
for (const member of statement.members) {
|
|
141
|
+
const memberName = propertyName(member.name);
|
|
142
|
+
if (!memberName) continue;
|
|
143
|
+
const key = `${statement.name.text}.${memberName}`;
|
|
144
|
+
const value = stringValue(member.initializer);
|
|
145
|
+
if (!stable) lookups.refusedMembers.set(key, refusalFact(
|
|
146
|
+
source, key, member, {
|
|
147
|
+
kind: 'enum_member', containerName: statement.name.text,
|
|
148
|
+
memberName, exported, stable,
|
|
149
|
+
reason: 'event_name_constant_container_unsafe_reference',
|
|
150
|
+
},
|
|
151
|
+
));
|
|
152
|
+
else if (!value) lookups.refusedMembers.set(key, refusalFact(
|
|
153
|
+
source, key, member, {
|
|
154
|
+
kind: 'enum_member', containerName: statement.name.text,
|
|
155
|
+
memberName, exported, stable,
|
|
156
|
+
reason: 'event_name_constant_member_not_string',
|
|
157
|
+
},
|
|
158
|
+
));
|
|
159
|
+
else lookups.enumMembers.set(key, staticFact(
|
|
160
|
+
source, key, value, member, {
|
|
161
|
+
kind: 'enum_member',
|
|
162
|
+
containerName: statement.name.text,
|
|
163
|
+
memberName,
|
|
164
|
+
exported,
|
|
165
|
+
stable,
|
|
166
|
+
},
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function objectLiteral(
|
|
172
|
+
expression: ts.Expression,
|
|
173
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
174
|
+
const value = unwrapExpression(expression);
|
|
175
|
+
return ts.isObjectLiteralExpression(value) ? value : undefined;
|
|
176
|
+
}
|
|
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
|
+
|
|
216
|
+
function collectObject(
|
|
217
|
+
source: ts.SourceFile,
|
|
218
|
+
statement: ts.VariableStatement,
|
|
219
|
+
declaration: ts.VariableDeclaration,
|
|
220
|
+
exports: Set<string>,
|
|
221
|
+
lookups: StringConstantLookups,
|
|
222
|
+
): void {
|
|
223
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
|
|
224
|
+
const object = objectLiteral(declaration.initializer);
|
|
225
|
+
if (!object) return;
|
|
226
|
+
const stable = stableLocalValueReference(source, declaration.name);
|
|
227
|
+
const exported = hasExportModifier(statement)
|
|
228
|
+
|| exports.has(declaration.name.text);
|
|
229
|
+
if (!supportedObjectShape(object)) {
|
|
230
|
+
refuseObjectShape(source, object, declaration, exported, lookups);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
for (const property of object.properties) {
|
|
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(
|
|
255
|
+
source, key, property, {
|
|
256
|
+
kind: 'const_object_property', containerName, memberName,
|
|
257
|
+
exported, stable,
|
|
258
|
+
reason: stable
|
|
259
|
+
? 'event_name_constant_member_not_string'
|
|
260
|
+
: 'event_name_constant_container_unsafe_reference',
|
|
261
|
+
},
|
|
262
|
+
));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
lookups.objectProperties.set(key, staticFact(
|
|
266
|
+
source, key, value, property, {
|
|
267
|
+
kind: 'const_object_property', containerName, memberName,
|
|
268
|
+
exported, stable,
|
|
269
|
+
},
|
|
270
|
+
));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function collectIdentifier(
|
|
274
|
+
source: ts.SourceFile,
|
|
275
|
+
statement: ts.VariableStatement,
|
|
276
|
+
declaration: ts.VariableDeclaration,
|
|
277
|
+
exports: Set<string>,
|
|
278
|
+
lookups: StringConstantLookups,
|
|
279
|
+
): void {
|
|
280
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
|
|
281
|
+
const value = stringValue(declaration.initializer);
|
|
282
|
+
if (!value) return;
|
|
283
|
+
const key = declaration.name.text;
|
|
284
|
+
lookups.identifiers.set(key, staticFact(
|
|
285
|
+
source, key, value, declaration, {
|
|
286
|
+
kind: 'const_identifier',
|
|
287
|
+
exported: hasExportModifier(statement) || exports.has(key),
|
|
288
|
+
stable: true,
|
|
289
|
+
},
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function collectStringConstantLookups(
|
|
294
|
+
source: ts.SourceFile,
|
|
295
|
+
): StringConstantLookups {
|
|
296
|
+
const lookups: StringConstantLookups = {
|
|
297
|
+
identifiers: new Map(),
|
|
298
|
+
enumMembers: new Map(),
|
|
299
|
+
objectProperties: new Map(),
|
|
300
|
+
refusedMembers: new Map(),
|
|
301
|
+
};
|
|
302
|
+
const exports = exportedNames(source);
|
|
303
|
+
for (const statement of source.statements) {
|
|
304
|
+
if (ts.isEnumDeclaration(statement))
|
|
305
|
+
collectEnum(source, statement, exports, lookups);
|
|
306
|
+
if (!ts.isVariableStatement(statement)
|
|
307
|
+
|| (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
308
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
309
|
+
collectIdentifier(source, statement, declaration, exports, lookups);
|
|
310
|
+
collectObject(source, statement, declaration, exports, lookups);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return lookups;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function resolveStringConstant(
|
|
317
|
+
expression: ts.Expression,
|
|
318
|
+
lookups: StringConstantLookups,
|
|
319
|
+
): StaticStringLookupResult {
|
|
320
|
+
const value = unwrapExpression(expression);
|
|
321
|
+
if (ts.isIdentifier(value)) {
|
|
322
|
+
const constant = lookups.identifiers.get(value.text);
|
|
323
|
+
return constant && constantReferenceMatches(value, constant)
|
|
324
|
+
? { status: 'resolved', constant } : { status: 'not_found' };
|
|
325
|
+
}
|
|
326
|
+
if (ts.isElementAccessExpression(value)
|
|
327
|
+
&& ts.isIdentifier(value.expression)) {
|
|
328
|
+
const prefix = `${value.expression.text}.`;
|
|
329
|
+
const known = [
|
|
330
|
+
...lookups.enumMembers.keys(),
|
|
331
|
+
...lookups.objectProperties.keys(),
|
|
332
|
+
...lookups.refusedMembers.keys(),
|
|
333
|
+
].some((key) => key.startsWith(prefix));
|
|
334
|
+
return known && constantContainerMatches(
|
|
335
|
+
value.expression, lookups, prefix,
|
|
336
|
+
) ? {
|
|
337
|
+
status: 'refused',
|
|
338
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
339
|
+
} : { status: 'not_found' };
|
|
340
|
+
}
|
|
341
|
+
if (!ts.isPropertyAccessExpression(value)
|
|
342
|
+
|| value.questionDotToken || !ts.isIdentifier(value.expression))
|
|
343
|
+
return { status: 'not_found' };
|
|
344
|
+
const key = `${value.expression.text}.${value.name.text}`;
|
|
345
|
+
const constant = lookups.enumMembers.get(key)
|
|
346
|
+
?? lookups.objectProperties.get(key);
|
|
347
|
+
if (constant && constantReferenceMatches(value.expression, constant))
|
|
348
|
+
return { status: 'resolved', constant };
|
|
349
|
+
const reason = lookups.refusedMembers.get(key);
|
|
350
|
+
return reason && refusalReferenceMatches(value.expression, reason)
|
|
351
|
+
? { status: 'refused', reason: reason.reason }
|
|
352
|
+
: constant || reason ? {
|
|
353
|
+
status: 'refused',
|
|
354
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
355
|
+
} : { status: 'not_found' };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function declarationContains(
|
|
359
|
+
identifier: ts.Identifier,
|
|
360
|
+
startOffset: number,
|
|
361
|
+
endOffset: number,
|
|
362
|
+
): boolean {
|
|
363
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
364
|
+
if (!declaration) return false;
|
|
365
|
+
const owner = declaration.parent;
|
|
366
|
+
return owner.getSourceFile() === identifier.getSourceFile()
|
|
367
|
+
&& owner.getStart(identifier.getSourceFile()) <= startOffset
|
|
368
|
+
&& owner.getEnd() >= endOffset;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function constantReferenceMatches(
|
|
372
|
+
identifier: ts.Identifier,
|
|
373
|
+
constant: StaticStringConstant,
|
|
374
|
+
): boolean {
|
|
375
|
+
return declarationContains(
|
|
376
|
+
identifier,
|
|
377
|
+
constant.declarationStartOffset,
|
|
378
|
+
constant.declarationEndOffset,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function refusalReferenceMatches(
|
|
383
|
+
identifier: ts.Identifier,
|
|
384
|
+
refusal: StaticStringRefusal,
|
|
385
|
+
): boolean {
|
|
386
|
+
return declarationContains(
|
|
387
|
+
identifier,
|
|
388
|
+
refusal.declarationStartOffset,
|
|
389
|
+
refusal.declarationEndOffset,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function constantContainerMatches(
|
|
394
|
+
identifier: ts.Identifier,
|
|
395
|
+
lookups: StringConstantLookups,
|
|
396
|
+
prefix: string,
|
|
397
|
+
): boolean {
|
|
398
|
+
const constants = [
|
|
399
|
+
...lookups.enumMembers.values(),
|
|
400
|
+
...lookups.objectProperties.values(),
|
|
401
|
+
].filter((item) => item.key.startsWith(prefix));
|
|
402
|
+
const refusals = [...lookups.refusedMembers.values()]
|
|
403
|
+
.filter((item) => item.key.startsWith(prefix));
|
|
404
|
+
return [...constants, ...refusals].some((item) =>
|
|
405
|
+
declarationContains(
|
|
406
|
+
identifier, item.declarationStartOffset, item.declarationEndOffset,
|
|
407
|
+
));
|
|
408
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -14,6 +14,12 @@ import type {
|
|
|
14
14
|
TraversalScopeState,
|
|
15
15
|
} from './traversal-scope.js';
|
|
16
16
|
import type { TraceGraphRow } from './evidence.js';
|
|
17
|
+
import {
|
|
18
|
+
eventMissingVariableNames,
|
|
19
|
+
eventTemplateVariables,
|
|
20
|
+
parseEventSkeletonFact,
|
|
21
|
+
type EventSkeletonFact,
|
|
22
|
+
} from '../utils/event-skeleton.js';
|
|
17
23
|
|
|
18
24
|
export interface EventRuntimeResolution {
|
|
19
25
|
row: TraceGraphRow;
|
|
@@ -37,6 +43,12 @@ function eventTemplate(evidence: Record<string, unknown>): string | undefined {
|
|
|
37
43
|
? resolution.original : undefined;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
function eventSkeleton(
|
|
47
|
+
evidence: Record<string, unknown>,
|
|
48
|
+
): EventSkeletonFact | undefined {
|
|
49
|
+
return parseEventSkeletonFact(evidence.eventSkeleton);
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
function missingReason(substitution: RuntimeSubstitution): string {
|
|
41
53
|
return `Dynamic target requires runtime variable overrides: ${
|
|
42
54
|
substitution.missing.join(', ')}`;
|
|
@@ -88,6 +100,10 @@ function eventEvidence(
|
|
|
88
100
|
unresolvedReason,
|
|
89
101
|
edgeType: row.edge_type,
|
|
90
102
|
};
|
|
103
|
+
const skeleton = eventSkeleton(evidence);
|
|
104
|
+
const missing = eventMissingVariableNames(
|
|
105
|
+
skeleton, substitution.missing,
|
|
106
|
+
);
|
|
91
107
|
return {
|
|
92
108
|
...evidence,
|
|
93
109
|
runtimeSubstitutions: { eventName: substitution },
|
|
@@ -96,8 +112,10 @@ function eventEvidence(
|
|
|
96
112
|
),
|
|
97
113
|
...(substitution.supplied.length > 0
|
|
98
114
|
? { runtimeVariablesApplied: true } : {}),
|
|
99
|
-
...(
|
|
100
|
-
|
|
115
|
+
...(missing.length > 0 ? {
|
|
116
|
+
missingRuntimeVariables: missing,
|
|
117
|
+
missingVariableCount: missing.length,
|
|
118
|
+
} : {}),
|
|
101
119
|
effectiveResolution,
|
|
102
120
|
linker: {
|
|
103
121
|
status: effectiveResolution.status,
|
|
@@ -137,7 +155,10 @@ export function runtimeEventResolution(
|
|
|
137
155
|
const template = eventTemplate(evidence);
|
|
138
156
|
if (!template || row.to_kind === 'event' && variables === undefined)
|
|
139
157
|
return undefined;
|
|
140
|
-
const
|
|
158
|
+
const skeleton = eventSkeleton(evidence);
|
|
159
|
+
const substitution = substituteVariables(
|
|
160
|
+
template, eventTemplateVariables(skeleton, variables ?? {}),
|
|
161
|
+
);
|
|
141
162
|
const effectiveRow = runtimeEventRow(
|
|
142
163
|
row, evidence, substitution, template,
|
|
143
164
|
);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import type { TraceOptions } from '../types.js';
|
|
3
|
+
import {
|
|
4
|
+
operationNode,
|
|
5
|
+
symbolNode,
|
|
6
|
+
type TraceGraphEdgeRow,
|
|
7
|
+
} from './trace-graph-lookups.js';
|
|
8
|
+
import type { TraceGraphRow } from './evidence.js';
|
|
9
|
+
import {
|
|
10
|
+
eventMissingVariableNames,
|
|
11
|
+
eventTemplateVariables,
|
|
12
|
+
parseEventSkeletonFact,
|
|
13
|
+
} from '../utils/event-skeleton.js';
|
|
14
|
+
|
|
15
|
+
const defaultEventShapeCandidateCap = 5;
|
|
16
|
+
const maximumEventShapeCandidateCap = 50;
|
|
17
|
+
|
|
18
|
+
function candidateCap(options: TraceOptions): number {
|
|
19
|
+
const value = options.maxDynamicCandidates
|
|
20
|
+
?? defaultEventShapeCandidateCap;
|
|
21
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
22
|
+
return defaultEventShapeCandidateCap;
|
|
23
|
+
return Math.min(value, maximumEventShapeCandidateCap);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function withCandidateCounts(
|
|
27
|
+
row: TraceGraphEdgeRow,
|
|
28
|
+
total: number,
|
|
29
|
+
shown: number,
|
|
30
|
+
): TraceGraphEdgeRow {
|
|
31
|
+
let evidence: Record<string, unknown> = {};
|
|
32
|
+
try {
|
|
33
|
+
const parsed: unknown = JSON.parse(row.evidence_json);
|
|
34
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
35
|
+
evidence = parsed as Record<string, unknown>;
|
|
36
|
+
} catch {
|
|
37
|
+
evidence = {};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...row,
|
|
41
|
+
evidence_json: JSON.stringify({
|
|
42
|
+
...evidence,
|
|
43
|
+
eventShapeCandidateCount: total,
|
|
44
|
+
shownEventShapeCandidateCount: shown,
|
|
45
|
+
omittedEventShapeCandidateCount: Math.max(0, total - shown),
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function visibleEventShapeRows(
|
|
51
|
+
rows: readonly TraceGraphEdgeRow[],
|
|
52
|
+
options: TraceOptions,
|
|
53
|
+
): TraceGraphEdgeRow[] {
|
|
54
|
+
const regular = rows.filter((row) =>
|
|
55
|
+
row.edge_type !== 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER');
|
|
56
|
+
if ((options.dynamicMode ?? 'strict') !== 'candidates') return regular;
|
|
57
|
+
const candidates = rows.filter((row) =>
|
|
58
|
+
row.edge_type === 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER');
|
|
59
|
+
const shown = candidates.slice(0, candidateCap(options));
|
|
60
|
+
return [
|
|
61
|
+
...regular,
|
|
62
|
+
...shown.map((row) =>
|
|
63
|
+
withCandidateCounts(row, candidates.length, shown.length)),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function outboundTraceEdgeType(
|
|
68
|
+
call: { call_type: string },
|
|
69
|
+
row: { edge_type: string; to_kind: string },
|
|
70
|
+
): string {
|
|
71
|
+
if (row.edge_type === 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER')
|
|
72
|
+
return 'event_shape_candidate_subscriber';
|
|
73
|
+
if (row.to_kind === 'operation'
|
|
74
|
+
&& row.edge_type === 'REMOTE_CALL_RESOLVES_TO_OPERATION')
|
|
75
|
+
return 'remote_action';
|
|
76
|
+
if (row.to_kind === 'operation'
|
|
77
|
+
&& row.edge_type === 'LOCAL_CALL_RESOLVES_TO_OPERATION')
|
|
78
|
+
return 'local_service_call';
|
|
79
|
+
return call.call_type;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function outboundTraceTargetNode(
|
|
83
|
+
db: Db,
|
|
84
|
+
id: string,
|
|
85
|
+
row: TraceGraphRow,
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
const operation = row.to_kind === 'operation'
|
|
88
|
+
? operationNode(db, row.to_id) : undefined;
|
|
89
|
+
const candidate = row.edge_type === 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER'
|
|
90
|
+
? symbolNode(db, Number(row.to_id)) : undefined;
|
|
91
|
+
return operation ?? candidate ?? {
|
|
92
|
+
id,
|
|
93
|
+
kind: row.to_kind,
|
|
94
|
+
label: row.to_kind === 'db_entity'
|
|
95
|
+
? `Entity: ${row.to_id || 'unknown'}` : row.to_id,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function stringArray(value: unknown): string[] {
|
|
100
|
+
return Array.isArray(value)
|
|
101
|
+
? value.filter((item): item is string => typeof item === 'string') : [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function subscriberSkeletons(
|
|
105
|
+
db: Db,
|
|
106
|
+
workspaceId: number,
|
|
107
|
+
publishCallId: number,
|
|
108
|
+
): unknown[] {
|
|
109
|
+
return db.prepare(`SELECT subscription.event_skeleton_json skeleton
|
|
110
|
+
FROM outbound_calls publication
|
|
111
|
+
JOIN repositories publication_repo ON publication_repo.id=publication.repo_id
|
|
112
|
+
JOIN outbound_calls subscription
|
|
113
|
+
ON subscription.call_type='async_subscribe'
|
|
114
|
+
AND subscription.event_skeleton_signature
|
|
115
|
+
=publication.event_skeleton_signature
|
|
116
|
+
JOIN repositories subscription_repo
|
|
117
|
+
ON subscription_repo.id=subscription.repo_id
|
|
118
|
+
AND subscription_repo.workspace_id=publication_repo.workspace_id
|
|
119
|
+
WHERE publication.id=? AND publication_repo.workspace_id=?
|
|
120
|
+
AND publication.event_skeleton_signature IS NOT NULL
|
|
121
|
+
ORDER BY subscription_repo.name COLLATE BINARY,
|
|
122
|
+
subscription.repo_id,subscription.source_file COLLATE BINARY,
|
|
123
|
+
subscription.call_site_start_offset,subscription.id`).all(
|
|
124
|
+
publishCallId, workspaceId,
|
|
125
|
+
).map((row) => row.skeleton);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function eventShapeMissingVariableEvidence(
|
|
129
|
+
db: Db,
|
|
130
|
+
workspaceId: number,
|
|
131
|
+
publishCallId: number,
|
|
132
|
+
evidence: Record<string, unknown>,
|
|
133
|
+
variables: Record<string, string> | undefined,
|
|
134
|
+
): Record<string, unknown> {
|
|
135
|
+
const current = stringArray(evidence.missingRuntimeVariables);
|
|
136
|
+
if (current.length === 0) return evidence;
|
|
137
|
+
const names = new Set(current);
|
|
138
|
+
let matchingSubscriptions = 0;
|
|
139
|
+
for (const value of subscriberSkeletons(db, workspaceId, publishCallId)) {
|
|
140
|
+
const skeleton = parseEventSkeletonFact(value);
|
|
141
|
+
if (!skeleton?.candidateEligible) continue;
|
|
142
|
+
matchingSubscriptions += 1;
|
|
143
|
+
const expanded = eventTemplateVariables(skeleton, variables ?? {});
|
|
144
|
+
const missing = skeleton.sourceKeys.filter((key) =>
|
|
145
|
+
!Object.hasOwn(expanded, key));
|
|
146
|
+
for (const name of eventMissingVariableNames(skeleton, missing))
|
|
147
|
+
names.add(name);
|
|
148
|
+
}
|
|
149
|
+
const missing = [...names].sort((left, right) =>
|
|
150
|
+
left < right ? -1 : left > right ? 1 : 0);
|
|
151
|
+
return {
|
|
152
|
+
...evidence,
|
|
153
|
+
missingRuntimeVariables: missing,
|
|
154
|
+
missingVariableCount: missing.length,
|
|
155
|
+
eventShapeMatchingSubscriptionCount: matchingSubscriptions,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function eventShapeRuntimeEvidence(
|
|
160
|
+
db: Db,
|
|
161
|
+
workspaceId: number,
|
|
162
|
+
callId: number,
|
|
163
|
+
callType: string,
|
|
164
|
+
evidence: Record<string, unknown>,
|
|
165
|
+
variables: Record<string, string> | undefined,
|
|
166
|
+
): Record<string, unknown> {
|
|
167
|
+
return callType === 'async_emit'
|
|
168
|
+
? eventShapeMissingVariableEvidence(
|
|
169
|
+
db, workspaceId, callId, evidence, variables,
|
|
170
|
+
)
|
|
171
|
+
: evidence;
|
|
172
|
+
}
|