@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,204 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { parseEventSkeletonFact } from '../utils/event-skeleton.js';
|
|
3
|
+
import { boundCandidateLikeEvidence } from '../utils/bounded-projection.js';
|
|
4
|
+
|
|
5
|
+
export interface EventShapeCandidateLinkSummary {
|
|
6
|
+
edgeCount: number;
|
|
7
|
+
omittedCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const EVENT_SHAPE_LINK_CAP_PER_EMIT = 100;
|
|
11
|
+
|
|
12
|
+
interface EventShapeRow extends Record<string, unknown> {
|
|
13
|
+
id: number;
|
|
14
|
+
repoId: number;
|
|
15
|
+
repoName: string;
|
|
16
|
+
signature: string;
|
|
17
|
+
skeletonJson: string;
|
|
18
|
+
evidenceJson: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SubscriberAssociation extends Record<string, unknown> {
|
|
22
|
+
graphEdgeId: number;
|
|
23
|
+
subscribeCallId: number;
|
|
24
|
+
targetKind: string;
|
|
25
|
+
targetId: string;
|
|
26
|
+
status: string;
|
|
27
|
+
evidenceJson: string;
|
|
28
|
+
targetLabel?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function eventRows(
|
|
32
|
+
db: Db,
|
|
33
|
+
workspaceId: number,
|
|
34
|
+
callType: 'async_emit' | 'async_subscribe',
|
|
35
|
+
): EventShapeRow[] {
|
|
36
|
+
return db.prepare(`SELECT c.id,c.repo_id repoId,r.name repoName,
|
|
37
|
+
c.event_skeleton_signature signature,
|
|
38
|
+
c.event_skeleton_json skeletonJson,c.evidence_json evidenceJson
|
|
39
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
40
|
+
WHERE r.workspace_id=? AND c.call_type=?
|
|
41
|
+
AND c.event_skeleton_signature IS NOT NULL
|
|
42
|
+
AND c.event_skeleton_json IS NOT NULL
|
|
43
|
+
ORDER BY c.event_skeleton_signature COLLATE BINARY,
|
|
44
|
+
r.name COLLATE BINARY,r.id,c.source_file COLLATE BINARY,
|
|
45
|
+
c.call_site_start_offset,c.call_site_end_offset,c.id`).all(
|
|
46
|
+
workspaceId, callType,
|
|
47
|
+
) as unknown as EventShapeRow[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function associations(
|
|
51
|
+
db: Db,
|
|
52
|
+
workspaceId: number,
|
|
53
|
+
generation: number,
|
|
54
|
+
): SubscriberAssociation[] {
|
|
55
|
+
return db.prepare(`SELECT edge.id graphEdgeId,
|
|
56
|
+
CAST(json_extract(edge.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
57
|
+
subscribeCallId,
|
|
58
|
+
edge.to_kind targetKind,edge.to_id targetId,edge.status,
|
|
59
|
+
edge.evidence_json evidenceJson,
|
|
60
|
+
target.source_file || ':' || target.qualified_name targetLabel
|
|
61
|
+
FROM graph_edges edge
|
|
62
|
+
LEFT JOIN symbols target ON edge.to_kind='symbol'
|
|
63
|
+
AND target.id=CAST(edge.to_id AS INTEGER)
|
|
64
|
+
WHERE edge.workspace_id=? AND edge.generation=?
|
|
65
|
+
AND edge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
66
|
+
AND edge.to_kind='symbol'
|
|
67
|
+
ORDER BY subscribeCallId,edge.id`).all(
|
|
68
|
+
workspaceId, generation,
|
|
69
|
+
) as unknown as SubscriberAssociation[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parsedEvidence(value: string): Record<string, unknown> {
|
|
73
|
+
try {
|
|
74
|
+
const parsed: unknown = JSON.parse(value);
|
|
75
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
76
|
+
? parsed as Record<string, unknown> : {};
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function candidateEligible(
|
|
83
|
+
emit: EventShapeRow,
|
|
84
|
+
subscribe: EventShapeRow,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (emit.signature !== subscribe.signature) return false;
|
|
87
|
+
const left = parseEventSkeletonFact(emit.skeletonJson);
|
|
88
|
+
const right = parseEventSkeletonFact(subscribe.skeletonJson);
|
|
89
|
+
return Boolean(left?.candidateEligible && right?.candidateEligible
|
|
90
|
+
&& left.holeCount === right.holeCount
|
|
91
|
+
&& JSON.stringify(left.literalSpans) === JSON.stringify(right.literalSpans));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function candidateEvidence(
|
|
95
|
+
emit: EventShapeRow,
|
|
96
|
+
subscribe: EventShapeRow,
|
|
97
|
+
association: SubscriberAssociation,
|
|
98
|
+
total: number,
|
|
99
|
+
shown: number,
|
|
100
|
+
): Record<string, unknown> {
|
|
101
|
+
const evidence = parsedEvidence(association.evidenceJson);
|
|
102
|
+
const parser = parsedEvidence(emit.evidenceJson);
|
|
103
|
+
return {
|
|
104
|
+
publishCallId: emit.id,
|
|
105
|
+
subscribeCallId: subscribe.id,
|
|
106
|
+
eventSkeletonSignature: emit.signature,
|
|
107
|
+
dispatchScope: 'workspace_event_name_only',
|
|
108
|
+
dispatchCertainty: 'skeleton_equivalent',
|
|
109
|
+
subscriptionRepositoryId: subscribe.repoId,
|
|
110
|
+
subscriptionRepositoryName: subscribe.repoName,
|
|
111
|
+
subscriptionConsumerRepositoryId:
|
|
112
|
+
evidence.subscriptionConsumerRepositoryId,
|
|
113
|
+
subscriptionConsumerRepositoryName:
|
|
114
|
+
evidence.subscriptionConsumerRepositoryName,
|
|
115
|
+
handlerSymbolId: Number(association.targetId),
|
|
116
|
+
eventShapeCandidateTargetLabel: association.targetLabel,
|
|
117
|
+
associationGraphEdgeId: association.graphEdgeId,
|
|
118
|
+
outboundEvidence: boundCandidateLikeEvidence(parser),
|
|
119
|
+
eventShapeLinkCandidateCount: total,
|
|
120
|
+
shownEventShapeLinkCandidateCount: shown,
|
|
121
|
+
omittedEventShapeLinkCandidateCount: Math.max(0, total - shown),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function insertCandidate(
|
|
126
|
+
db: Db,
|
|
127
|
+
workspaceId: number,
|
|
128
|
+
generation: number,
|
|
129
|
+
emit: EventShapeRow,
|
|
130
|
+
subscribe: EventShapeRow,
|
|
131
|
+
association: SubscriberAssociation,
|
|
132
|
+
total: number,
|
|
133
|
+
shown: number,
|
|
134
|
+
): void {
|
|
135
|
+
db.prepare(`INSERT INTO graph_edges(
|
|
136
|
+
workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
|
|
137
|
+
confidence,evidence_json,is_dynamic,unresolved_reason,generation
|
|
138
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
|
|
139
|
+
workspaceId,
|
|
140
|
+
'EVENT_SHAPE_CANDIDATE_SUBSCRIBER',
|
|
141
|
+
'dynamic',
|
|
142
|
+
'call',
|
|
143
|
+
String(emit.id),
|
|
144
|
+
association.targetKind,
|
|
145
|
+
association.targetId,
|
|
146
|
+
0.3,
|
|
147
|
+
JSON.stringify(candidateEvidence(
|
|
148
|
+
emit, subscribe, association, total, shown,
|
|
149
|
+
)),
|
|
150
|
+
1,
|
|
151
|
+
'event_skeleton_equivalent_non_authoritative',
|
|
152
|
+
generation,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface ShapeCandidate {
|
|
157
|
+
subscription: EventShapeRow;
|
|
158
|
+
association: SubscriberAssociation;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function candidatesForEmit(
|
|
162
|
+
emit: EventShapeRow,
|
|
163
|
+
subscriptions: readonly EventShapeRow[],
|
|
164
|
+
bySubscription: ReadonlyMap<number, SubscriberAssociation[]>,
|
|
165
|
+
): ShapeCandidate[] {
|
|
166
|
+
return subscriptions.flatMap((subscription) =>
|
|
167
|
+
!candidateEligible(emit, subscription) ? []
|
|
168
|
+
: (bySubscription.get(subscription.id) ?? []).map((association) => ({
|
|
169
|
+
subscription,
|
|
170
|
+
association,
|
|
171
|
+
})));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function linkEventShapeCandidates(
|
|
175
|
+
db: Db,
|
|
176
|
+
workspaceId: number,
|
|
177
|
+
generation: number,
|
|
178
|
+
): EventShapeCandidateLinkSummary {
|
|
179
|
+
const emits = eventRows(db, workspaceId, 'async_emit');
|
|
180
|
+
const subscriptions = eventRows(db, workspaceId, 'async_subscribe');
|
|
181
|
+
const bySubscription = new Map<number, SubscriberAssociation[]>();
|
|
182
|
+
for (const association of associations(db, workspaceId, generation))
|
|
183
|
+
bySubscription.set(association.subscribeCallId, [
|
|
184
|
+
...(bySubscription.get(association.subscribeCallId) ?? []),
|
|
185
|
+
association,
|
|
186
|
+
]);
|
|
187
|
+
let edgeCount = 0;
|
|
188
|
+
let omittedCount = 0;
|
|
189
|
+
for (const emit of emits) {
|
|
190
|
+
const candidates = candidatesForEmit(
|
|
191
|
+
emit, subscriptions, bySubscription,
|
|
192
|
+
);
|
|
193
|
+
const shown = candidates.slice(0, EVENT_SHAPE_LINK_CAP_PER_EMIT);
|
|
194
|
+
for (const candidate of shown) {
|
|
195
|
+
insertCandidate(
|
|
196
|
+
db, workspaceId, generation, emit, candidate.subscription,
|
|
197
|
+
candidate.association, candidates.length, shown.length,
|
|
198
|
+
);
|
|
199
|
+
edgeCount += 1;
|
|
200
|
+
}
|
|
201
|
+
omittedCount += Math.max(0, candidates.length - shown.length);
|
|
202
|
+
}
|
|
203
|
+
return { edgeCount, omittedCount };
|
|
204
|
+
}
|
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
linkEventTemplate,
|
|
4
4
|
type LinkedEventTemplate,
|
|
5
5
|
} from './event-template-link.js';
|
|
6
|
+
import {
|
|
7
|
+
subscriptionEnvironmentTargets,
|
|
8
|
+
type SubscriptionEnvironmentTarget,
|
|
9
|
+
} from './event-environment-link.js';
|
|
10
|
+
import { parseEventSkeletonFact } from '../utils/event-skeleton.js';
|
|
6
11
|
|
|
7
12
|
export interface SubscriptionHandlerLinkSummary {
|
|
8
13
|
edgeCount: number;
|
|
@@ -25,6 +30,10 @@ interface SubscriptionRow {
|
|
|
25
30
|
endOffset?: number | null;
|
|
26
31
|
confidence: number;
|
|
27
32
|
unresolvedReason?: string | null;
|
|
33
|
+
packageName?: string | null;
|
|
34
|
+
environmentJson?: string | null;
|
|
35
|
+
eventSkeletonJson?: string | null;
|
|
36
|
+
evidenceJson?: string | null;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
interface HandlerCallRow {
|
|
@@ -63,7 +72,11 @@ function subscriptionRows(db: Db, workspaceId: number): SubscriptionRow[] {
|
|
|
63
72
|
c.source_symbol_id sourceSymbolId,c.event_name_expr eventName,
|
|
64
73
|
c.source_file sourceFile,c.source_line sourceLine,
|
|
65
74
|
c.call_site_start_offset startOffset,c.call_site_end_offset endOffset,
|
|
66
|
-
c.confidence,c.unresolved_reason unresolvedReason
|
|
75
|
+
c.confidence,c.unresolved_reason unresolvedReason,
|
|
76
|
+
c.event_skeleton_json eventSkeletonJson,
|
|
77
|
+
c.evidence_json evidenceJson,
|
|
78
|
+
r.package_name packageName,
|
|
79
|
+
r.environment_declarations_json environmentJson
|
|
67
80
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
68
81
|
WHERE r.workspace_id=? AND c.call_type='async_subscribe'
|
|
69
82
|
AND json_extract(c.evidence_json,'$.handlerReferenceStatus')
|
|
@@ -208,11 +221,36 @@ function evidenceFor(
|
|
|
208
221
|
subscription: SubscriptionRow,
|
|
209
222
|
association: HandlerAssociation,
|
|
210
223
|
event: LinkedEventTemplate,
|
|
224
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
225
|
+
loopValue?: string,
|
|
211
226
|
): Record<string, unknown> {
|
|
212
227
|
const call: Partial<HandlerCallRow> = association.call ?? {};
|
|
213
228
|
const symbolCallReason = boundedSymbolCallReason(call.unresolvedReason);
|
|
229
|
+
const environmentAmbiguous = environment?.resolution.status === 'ambiguous'
|
|
230
|
+
|| Number(environment?.collisionCount ?? 1) > 1;
|
|
231
|
+
const resolutionStatus = event.isDynamic
|
|
232
|
+
? 'unresolved'
|
|
233
|
+
: environmentAmbiguous ? 'ambiguous' : association.status;
|
|
234
|
+
return {
|
|
235
|
+
...eventDispatchEvidence(subscription, event, environment, loopValue),
|
|
236
|
+
...handlerAssociationEvidence(
|
|
237
|
+
subscription, association, call, resolutionStatus,
|
|
238
|
+
environmentAmbiguous, environment,
|
|
239
|
+
),
|
|
240
|
+
...symbolCallReason,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function eventDispatchEvidence(
|
|
245
|
+
subscription: SubscriptionRow,
|
|
246
|
+
event: LinkedEventTemplate,
|
|
247
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
248
|
+
loopValue?: string,
|
|
249
|
+
): Record<string, unknown> {
|
|
214
250
|
return {
|
|
215
251
|
eventName: subscription.eventName,
|
|
252
|
+
materializedLoopEventName: loopValue,
|
|
253
|
+
...(eventSkeletonEvidence(subscription.eventSkeletonJson)),
|
|
216
254
|
...(event.substitution.placeholders.length > 0 ? {
|
|
217
255
|
effectiveEventName: event.targetId,
|
|
218
256
|
eventTemplateResolution: event.substitution,
|
|
@@ -220,12 +258,37 @@ function evidenceFor(
|
|
|
220
258
|
associationBasis: 'exact_subscription_call_span',
|
|
221
259
|
dispatchScope: 'workspace_event_name_only',
|
|
222
260
|
subscribeCallId: subscription.id,
|
|
261
|
+
repositoryId: subscription.repoId,
|
|
262
|
+
repositoryName: subscription.repoName,
|
|
263
|
+
subscriptionConsumerRepositoryId: environment?.consumerRepoId,
|
|
264
|
+
subscriptionConsumerRepositoryName: environment?.consumerRepoName,
|
|
265
|
+
dispatchCertainty: environment?.resolution.status === 'resolved'
|
|
266
|
+
? 'environment_declaration_exact' : 'static_name_only',
|
|
267
|
+
...(environment?.resolution.status === 'not_applicable' ? {} : {
|
|
268
|
+
eventEnvironmentResolution: {
|
|
269
|
+
status: environment?.resolution.status,
|
|
270
|
+
reason: environment?.resolution.reason,
|
|
271
|
+
provenance: environment?.resolution.provenance,
|
|
272
|
+
collisionCount: environment?.collisionCount,
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function handlerAssociationEvidence(
|
|
279
|
+
subscription: SubscriptionRow,
|
|
280
|
+
association: HandlerAssociation,
|
|
281
|
+
call: Partial<HandlerCallRow>,
|
|
282
|
+
resolutionStatus: string,
|
|
283
|
+
environmentAmbiguous: boolean,
|
|
284
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
285
|
+
): Record<string, unknown> {
|
|
286
|
+
return {
|
|
223
287
|
symbolCallId: call.id,
|
|
224
288
|
roleSiteMatchCount: association.matchCount,
|
|
225
|
-
callRole: association.matchCount > 0
|
|
289
|
+
callRole: association.matchCount > 0
|
|
290
|
+
? 'event_subscribe_handler' : undefined,
|
|
226
291
|
factOrigin: association.factOrigin ?? call.factOrigin,
|
|
227
|
-
repositoryId: subscription.repoId,
|
|
228
|
-
repositoryName: subscription.repoName,
|
|
229
292
|
sourceFile: subscription.sourceFile,
|
|
230
293
|
sourceLine: subscription.sourceLine,
|
|
231
294
|
callSiteStartOffset: subscription.startOffset,
|
|
@@ -237,14 +300,22 @@ function evidenceFor(
|
|
|
237
300
|
associationStatus: association.status,
|
|
238
301
|
symbolCallResolutionStatus:
|
|
239
302
|
association.symbolCallResolutionStatus ?? call.status,
|
|
240
|
-
resolutionStatus
|
|
303
|
+
resolutionStatus,
|
|
241
304
|
resolutionStrategy: call.strategy,
|
|
242
305
|
candidateCount: call.candidateCount,
|
|
243
|
-
reasonCode:
|
|
244
|
-
|
|
306
|
+
reasonCode: environmentAmbiguous
|
|
307
|
+
? environment?.resolution.reason ?? 'event_environment_value_collision'
|
|
308
|
+
: association.reasonCode,
|
|
245
309
|
};
|
|
246
310
|
}
|
|
247
311
|
|
|
312
|
+
function eventSkeletonEvidence(
|
|
313
|
+
value: string | null | undefined,
|
|
314
|
+
): Record<string, unknown> {
|
|
315
|
+
const skeleton = parseEventSkeletonFact(value);
|
|
316
|
+
return skeleton ? { eventSkeleton: skeleton } : {};
|
|
317
|
+
}
|
|
318
|
+
|
|
248
319
|
function boundedSymbolCallReason(
|
|
249
320
|
reason: string | null | undefined,
|
|
250
321
|
): Record<string, unknown> {
|
|
@@ -264,11 +335,14 @@ function insertAssociationEdge(
|
|
|
264
335
|
subscription: SubscriptionRow,
|
|
265
336
|
association: HandlerAssociation,
|
|
266
337
|
event: LinkedEventTemplate,
|
|
338
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
339
|
+
loopValue?: string,
|
|
267
340
|
): void {
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
341
|
+
const ambiguous = environmentTargetAmbiguous(environment);
|
|
342
|
+
const status = associationEdgeStatus(event, association, ambiguous);
|
|
343
|
+
const reason = associationEdgeReason(
|
|
344
|
+
event, association, environment, ambiguous,
|
|
345
|
+
);
|
|
272
346
|
db.prepare(`INSERT INTO graph_edges(
|
|
273
347
|
workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
|
|
274
348
|
confidence,evidence_json,is_dynamic,unresolved_reason,generation
|
|
@@ -281,13 +355,134 @@ function insertAssociationEdge(
|
|
|
281
355
|
association.toKind,
|
|
282
356
|
association.toId,
|
|
283
357
|
association.call?.confidence ?? subscription.confidence,
|
|
284
|
-
JSON.stringify(evidenceFor(
|
|
358
|
+
JSON.stringify(evidenceFor(
|
|
359
|
+
subscription, association, event, environment, loopValue,
|
|
360
|
+
)),
|
|
285
361
|
event.isDynamic ? 1 : 0,
|
|
286
362
|
reason,
|
|
287
363
|
generation,
|
|
288
364
|
);
|
|
289
365
|
}
|
|
290
366
|
|
|
367
|
+
function environmentTargetAmbiguous(
|
|
368
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
369
|
+
): boolean {
|
|
370
|
+
return environment?.resolution.status === 'ambiguous'
|
|
371
|
+
|| Number(environment?.collisionCount ?? 1) > 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function associationEdgeStatus(
|
|
375
|
+
event: LinkedEventTemplate,
|
|
376
|
+
association: HandlerAssociation,
|
|
377
|
+
environmentAmbiguous: boolean,
|
|
378
|
+
): 'resolved' | 'ambiguous' | 'unresolved' {
|
|
379
|
+
if (event.isDynamic) return 'unresolved';
|
|
380
|
+
return environmentAmbiguous ? 'ambiguous' : association.status;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function associationEdgeReason(
|
|
384
|
+
event: LinkedEventTemplate,
|
|
385
|
+
association: HandlerAssociation,
|
|
386
|
+
environment: SubscriptionEnvironmentTarget | undefined,
|
|
387
|
+
environmentAmbiguous: boolean,
|
|
388
|
+
): string | null {
|
|
389
|
+
if (event.isDynamic)
|
|
390
|
+
return event.substitution.missing.length > 0
|
|
391
|
+
? 'event_template_variables_missing'
|
|
392
|
+
: event.unresolvedReason ?? 'event_name_unsupported_constant_expression';
|
|
393
|
+
if (environmentAmbiguous)
|
|
394
|
+
return environment?.resolution.reason
|
|
395
|
+
?? 'event_environment_value_collision';
|
|
396
|
+
return association.reasonCode ?? association.call?.unresolvedReason ?? null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parsedEvidenceRecord(
|
|
400
|
+
value: string,
|
|
401
|
+
): Record<string, unknown> | undefined {
|
|
402
|
+
try {
|
|
403
|
+
const parsed: unknown = JSON.parse(value);
|
|
404
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
405
|
+
? parsed as Record<string, unknown> : undefined;
|
|
406
|
+
} catch {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function enumeratedLoopValues(
|
|
412
|
+
evidenceJson: string | null | undefined,
|
|
413
|
+
): string[] | undefined {
|
|
414
|
+
if (!evidenceJson) return undefined;
|
|
415
|
+
const evidence = parsedEvidenceRecord(evidenceJson);
|
|
416
|
+
if (!evidence
|
|
417
|
+
|| evidence.subscriptionLoopRegistrationStatus !== 'enumerated')
|
|
418
|
+
return undefined;
|
|
419
|
+
const values = evidence.subscriptionLoopValues;
|
|
420
|
+
if (!Array.isArray(values)
|
|
421
|
+
|| !values.every((item): item is string => typeof item === 'string'))
|
|
422
|
+
return undefined;
|
|
423
|
+
return evidence.subscriptionLoopRegistrationCount === values.length
|
|
424
|
+
&& evidence.omittedSubscriptionLoopValueCount === 0 ? values : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function linkedSubscriptionEvents(
|
|
428
|
+
db: Db,
|
|
429
|
+
workspaceId: number,
|
|
430
|
+
subscription: SubscriptionRow,
|
|
431
|
+
variables: Record<string, string>,
|
|
432
|
+
): Array<{
|
|
433
|
+
event: LinkedEventTemplate;
|
|
434
|
+
environment: SubscriptionEnvironmentTarget;
|
|
435
|
+
loopValue?: string;
|
|
436
|
+
}> {
|
|
437
|
+
const skeleton = parseEventSkeletonFact(subscription.eventSkeletonJson);
|
|
438
|
+
const environments = subscriptionEnvironmentTargets(
|
|
439
|
+
db,
|
|
440
|
+
workspaceId,
|
|
441
|
+
subscription.packageName ?? undefined,
|
|
442
|
+
subscription.eventSkeletonJson,
|
|
443
|
+
subscription.environmentJson,
|
|
444
|
+
variables,
|
|
445
|
+
);
|
|
446
|
+
const loopValues = enumeratedLoopValues(subscription.evidenceJson);
|
|
447
|
+
const templates = loopValues ?? [subscription.eventName];
|
|
448
|
+
return environments.flatMap((environment) =>
|
|
449
|
+
templates.map((template) => ({
|
|
450
|
+
environment,
|
|
451
|
+
loopValue: loopValues ? template : undefined,
|
|
452
|
+
event: linkEventTemplate(
|
|
453
|
+
template,
|
|
454
|
+
environment.resolution.variables,
|
|
455
|
+
loopValues ? undefined : subscription.unresolvedReason ?? undefined,
|
|
456
|
+
loopValues ? undefined : skeleton,
|
|
457
|
+
),
|
|
458
|
+
})));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function targetStatus(
|
|
462
|
+
association: HandlerAssociation,
|
|
463
|
+
event: LinkedEventTemplate,
|
|
464
|
+
environment: SubscriptionEnvironmentTarget,
|
|
465
|
+
): 'resolved' | 'ambiguous' | 'unresolved' {
|
|
466
|
+
if (event.isDynamic || association.status === 'unresolved')
|
|
467
|
+
return 'unresolved';
|
|
468
|
+
if (association.status === 'ambiguous'
|
|
469
|
+
|| environment.resolution.status === 'ambiguous'
|
|
470
|
+
|| environment.collisionCount > 1) return 'ambiguous';
|
|
471
|
+
return 'resolved';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function incrementSummary(
|
|
475
|
+
summary: SubscriptionHandlerLinkSummary,
|
|
476
|
+
status: 'resolved' | 'ambiguous' | 'unresolved',
|
|
477
|
+
missing: boolean,
|
|
478
|
+
): void {
|
|
479
|
+
summary.edgeCount += 1;
|
|
480
|
+
summary.resolvedCount += status === 'resolved' ? 1 : 0;
|
|
481
|
+
summary.ambiguousCount += status === 'ambiguous' ? 1 : 0;
|
|
482
|
+
summary.unresolvedCount += status === 'unresolved' ? 1 : 0;
|
|
483
|
+
summary.missingAssociationCount += missing ? 1 : 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
291
486
|
export function linkEventSubscriptionHandlers(
|
|
292
487
|
db: Db,
|
|
293
488
|
workspaceId: number,
|
|
@@ -302,22 +497,22 @@ export function linkEventSubscriptionHandlers(
|
|
|
302
497
|
missingAssociationCount: 0,
|
|
303
498
|
};
|
|
304
499
|
for (const subscription of subscriptionRows(db, workspaceId)) {
|
|
305
|
-
const event = linkEventTemplate(
|
|
306
|
-
subscription.eventName, variables,
|
|
307
|
-
subscription.unresolvedReason ?? undefined,
|
|
308
|
-
);
|
|
309
500
|
const association = associationFor(
|
|
310
501
|
subscription, roleSiteRows(db, subscription),
|
|
311
502
|
);
|
|
312
|
-
|
|
313
|
-
db, workspaceId,
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
503
|
+
for (const target of linkedSubscriptionEvents(
|
|
504
|
+
db, workspaceId, subscription, variables,
|
|
505
|
+
)) {
|
|
506
|
+
insertAssociationEdge(
|
|
507
|
+
db, workspaceId, generation, subscription, association,
|
|
508
|
+
target.event, target.environment, target.loopValue,
|
|
509
|
+
);
|
|
510
|
+
incrementSummary(
|
|
511
|
+
summary,
|
|
512
|
+
targetStatus(association, target.event, target.environment),
|
|
513
|
+
association.missing,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
321
516
|
}
|
|
322
517
|
return summary;
|
|
323
518
|
}
|
|
@@ -3,6 +3,14 @@ import {
|
|
|
3
3
|
substituteVariables,
|
|
4
4
|
type RuntimeSubstitution,
|
|
5
5
|
} from './dynamic-edge-resolver.js';
|
|
6
|
+
import {
|
|
7
|
+
resolveEventEnvironment,
|
|
8
|
+
} from './event-environment-link.js';
|
|
9
|
+
import {
|
|
10
|
+
eventTemplateVariables,
|
|
11
|
+
parseEventSkeletonFact,
|
|
12
|
+
type EventSkeletonFact,
|
|
13
|
+
} from '../utils/event-skeleton.js';
|
|
6
14
|
|
|
7
15
|
export interface LinkedEventTemplate {
|
|
8
16
|
targetId: string;
|
|
@@ -17,8 +25,11 @@ export function linkEventTemplate(
|
|
|
17
25
|
template: string,
|
|
18
26
|
variables: Record<string, string>,
|
|
19
27
|
parserReason?: string,
|
|
28
|
+
skeleton?: EventSkeletonFact,
|
|
20
29
|
): LinkedEventTemplate {
|
|
21
|
-
const substitution = substituteVariables(
|
|
30
|
+
const substitution = substituteVariables(
|
|
31
|
+
template, eventTemplateVariables(skeleton, variables),
|
|
32
|
+
);
|
|
22
33
|
const missing = substitution.missing.length > 0;
|
|
23
34
|
const unsupportedDynamic = substitution.placeholders.length === 0
|
|
24
35
|
&& parserReason !== undefined;
|
|
@@ -47,18 +58,41 @@ export function insertEventCallEdge(
|
|
|
47
58
|
evidence: Record<string, unknown>,
|
|
48
59
|
): { status: string; callType: string } {
|
|
49
60
|
const callType = String(call.call_type);
|
|
61
|
+
const skeleton = parseEventSkeletonFact(call.event_skeleton_json);
|
|
62
|
+
const environment = resolveEventEnvironment(
|
|
63
|
+
call.event_skeleton_json,
|
|
64
|
+
call.environmentDeclarationsJson,
|
|
65
|
+
variables,
|
|
66
|
+
);
|
|
50
67
|
const event = linkEventTemplate(
|
|
51
|
-
String(call.event_name_expr ?? ''), variables,
|
|
68
|
+
String(call.event_name_expr ?? ''), environment.variables,
|
|
52
69
|
typeof call.unresolved_reason === 'string'
|
|
53
70
|
? call.unresolved_reason : undefined,
|
|
71
|
+
skeleton,
|
|
54
72
|
);
|
|
55
73
|
const edgeType = event.isDynamic
|
|
56
74
|
? 'DYNAMIC_EDGE_CANDIDATE'
|
|
57
75
|
: callType === 'async_emit'
|
|
58
76
|
? 'HANDLER_EMITS_EVENT' : 'EVENT_CONSUMED_BY_HANDLER';
|
|
59
|
-
const eventEvidence =
|
|
60
|
-
|
|
61
|
-
:
|
|
77
|
+
const eventEvidence = {
|
|
78
|
+
...evidence,
|
|
79
|
+
dispatchScope: 'workspace_event_name_only',
|
|
80
|
+
dispatchCertainty: environment.status === 'resolved'
|
|
81
|
+
? 'environment_declaration_exact'
|
|
82
|
+
: event.isDynamic ? 'runtime_variables_required' : 'static_name_only',
|
|
83
|
+
...(skeleton ? { eventSkeleton: skeleton } : {}),
|
|
84
|
+
...(event.substitution.placeholders.length > 0
|
|
85
|
+
? { eventTemplateResolution: event.substitution } : {}),
|
|
86
|
+
...(environment.status === 'not_applicable' ? {} : {
|
|
87
|
+
eventEnvironmentResolution: {
|
|
88
|
+
status: environment.status,
|
|
89
|
+
reason: environment.reason,
|
|
90
|
+
provenance: environment.provenance,
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
const unresolvedReason = environment.reason
|
|
95
|
+
?? event.unresolvedReason;
|
|
62
96
|
db.prepare(`INSERT INTO graph_edges(
|
|
63
97
|
workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
|
|
64
98
|
confidence,evidence_json,is_dynamic,unresolved_reason,generation
|
|
@@ -66,7 +100,7 @@ export function insertEventCallEdge(
|
|
|
66
100
|
workspaceId, edgeType, event.status, 'call', String(call.id),
|
|
67
101
|
event.targetKind, event.targetId, Number(call.confidence ?? 0.2),
|
|
68
102
|
JSON.stringify(eventEvidence), event.isDynamic ? 1 : 0,
|
|
69
|
-
|
|
103
|
+
unresolvedReason ?? null, generation,
|
|
70
104
|
);
|
|
71
105
|
return { status: event.status, callType };
|
|
72
106
|
}
|