@saptools/service-flow 0.1.70 → 0.1.72
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 +13 -0
- package/README.md +9 -5
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-GSLFY6J2.js → chunk-Z6D433R5.js} +8696 -6476
- package/dist/chunk-Z6D433R5.js.map +1 -0
- package/dist/cli.js +685 -142
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +53 -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 +371 -0
- package/src/cli/doctor.ts +4 -6
- package/src/cli.ts +1 -1
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -5
- package/src/db/event-fact-semantics.ts +347 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/migrations.ts +12 -1
- package/src/db/package-target-invalidation.ts +79 -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 +45 -6
- package/src/linker/cross-repo-linker.ts +25 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +161 -0
- package/src/linker/event-subscription-handler-linker.ts +123 -19
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +298 -0
- package/src/output/table-output.ts +13 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +327 -0
- package/src/parsers/event-call-analysis.ts +242 -0
- package/src/parsers/event-environment-reference.ts +231 -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 +404 -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 +30 -9
- package/src/parsers/string-constant-lookups.ts +358 -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 +19 -7
- package/src/trace/evidence.ts +7 -0
- package/src/trace/trace-scope-execution.ts +21 -29
- package/src/types.ts +17 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
type TraversalScopeScheduler,
|
|
9
9
|
type TraversalScopeState,
|
|
10
10
|
} from './traversal-scope.js';
|
|
11
|
+
import {
|
|
12
|
+
eventTemplateVariables,
|
|
13
|
+
parseEventSkeletonFact,
|
|
14
|
+
} from '../utils/event-skeleton.js';
|
|
11
15
|
|
|
12
16
|
export type EventSubscriberTransitionStatus =
|
|
13
17
|
| 'resolved'
|
|
@@ -96,7 +100,8 @@ export function planEventSubscriberTransitions(
|
|
|
96
100
|
): PlannedEventSubscriberTransition[] {
|
|
97
101
|
return loadEventSubscriberTransitions(db, query).map((transition) => {
|
|
98
102
|
const handler = transition.handler;
|
|
99
|
-
if (
|
|
103
|
+
if (transition.status !== 'resolved' || !handler)
|
|
104
|
+
return plannedTransition(transition, 'not_resolved');
|
|
100
105
|
if (depth >= maxDepth)
|
|
101
106
|
return plannedTransition(transition, 'depth_limited');
|
|
102
107
|
const state = scheduler.schedule({
|
|
@@ -245,13 +250,15 @@ function eventRowForQuery(
|
|
|
245
250
|
row: Record<string, unknown>,
|
|
246
251
|
query: EventSubscriberTransitionQuery,
|
|
247
252
|
): Record<string, unknown> | undefined {
|
|
253
|
+
const exact = exactPersistedEventRow(row, query);
|
|
254
|
+
if (exact) return exact;
|
|
248
255
|
const variables = query.vars;
|
|
249
|
-
if (variables === undefined) return
|
|
256
|
+
if (variables === undefined) return undefined;
|
|
250
257
|
const evidence = parseEvidence(row.evidenceJson);
|
|
251
258
|
const resolution = isRecord(evidence.eventTemplateResolution)
|
|
252
259
|
? evidence.eventTemplateResolution : {};
|
|
253
260
|
if (typeof resolution.original !== 'string')
|
|
254
|
-
return
|
|
261
|
+
return undefined;
|
|
255
262
|
return runtimeMatchedEventRow(
|
|
256
263
|
row, query, evidence, resolution.original, variables,
|
|
257
264
|
);
|
|
@@ -272,7 +279,10 @@ function runtimeMatchedEventRow(
|
|
|
272
279
|
template: string,
|
|
273
280
|
variables: Record<string, string>,
|
|
274
281
|
): Record<string, unknown> | undefined {
|
|
275
|
-
const
|
|
282
|
+
const skeleton = parseEventSkeletonFact(evidence.eventSkeleton);
|
|
283
|
+
const substitution = substituteVariables(
|
|
284
|
+
template, eventTemplateVariables(skeleton, variables),
|
|
285
|
+
);
|
|
276
286
|
if (substitution.missing.length > 0
|
|
277
287
|
|| substitution.effective !== query.eventName) return undefined;
|
|
278
288
|
const resolvedAssociation = evidence.associationStatus === 'resolved';
|
|
@@ -295,8 +305,7 @@ export function eventSubscriberMissingVariables(
|
|
|
295
305
|
db: Db,
|
|
296
306
|
query: EventSubscriberTransitionQuery,
|
|
297
307
|
): string[] {
|
|
298
|
-
|
|
299
|
-
const variables = query.vars;
|
|
308
|
+
const variables = query.vars ?? {};
|
|
300
309
|
const rows = db.prepare(`SELECT ge.from_id eventName,
|
|
301
310
|
ge.evidence_json evidenceJson FROM graph_edges ge
|
|
302
311
|
WHERE ge.workspace_id=? AND ge.generation=?
|
|
@@ -311,7 +320,10 @@ export function eventSubscriberMissingVariables(
|
|
|
311
320
|
const template = typeof resolution.original === 'string'
|
|
312
321
|
? resolution.original : undefined;
|
|
313
322
|
if (!template) return [];
|
|
314
|
-
const
|
|
323
|
+
const skeleton = parseEventSkeletonFact(evidence.eventSkeleton);
|
|
324
|
+
const substitution = substituteVariables(
|
|
325
|
+
template, eventTemplateVariables(skeleton, variables),
|
|
326
|
+
);
|
|
315
327
|
if (substitution.missing.length === 0
|
|
316
328
|
|| matchRuntimeTemplate(
|
|
317
329
|
substitution.effective, query.eventName,
|
package/src/trace/evidence.ts
CHANGED
|
@@ -262,6 +262,8 @@ function runtimeDiagnosticTotals(
|
|
|
262
262
|
const effective = parseObject(edge.evidence.effectiveResolution);
|
|
263
263
|
if (!['dynamic', 'unresolved', 'ambiguous'].includes(String(effective.status ?? '')))
|
|
264
264
|
continue;
|
|
265
|
+
for (const key of stringArray(edge.evidence.missingRuntimeVariables))
|
|
266
|
+
missing.add(key);
|
|
265
267
|
const substitutions = edge.evidence.runtimeSubstitutions;
|
|
266
268
|
if (!substitutions || typeof substitutions !== 'object' || Array.isArray(substitutions)) continue;
|
|
267
269
|
for (const value of Object.values(substitutions as Record<string, RuntimeSubstitution>))
|
|
@@ -648,6 +650,11 @@ function stringValue(value: unknown): string | undefined {
|
|
|
648
650
|
return typeof value === 'string' ? value : undefined;
|
|
649
651
|
}
|
|
650
652
|
|
|
653
|
+
function stringArray(value: unknown): string[] {
|
|
654
|
+
return Array.isArray(value)
|
|
655
|
+
? value.filter((item): item is string => typeof item === 'string') : [];
|
|
656
|
+
}
|
|
657
|
+
|
|
651
658
|
function numeric(value: unknown): number {
|
|
652
659
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
653
660
|
}
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import type { PlannedEventSubscriberTransition } from './event-subscriber-traversal.js';
|
|
21
21
|
import {
|
|
22
22
|
graphForCalls,
|
|
23
|
-
operationNode,
|
|
24
23
|
symbolNode,
|
|
25
24
|
type TraceGraphEdgeRow,
|
|
26
25
|
} from './trace-graph-lookups.js';
|
|
@@ -51,6 +50,12 @@ import type { ImplementationHintOptions } from './trace-implementation-scope.js'
|
|
|
51
50
|
import { processOperationTarget } from './trace-operation-execution.js';
|
|
52
51
|
import { runtimeEventResolution, runtimeEventSubscriberPlans } from './event-runtime-resolution.js';
|
|
53
52
|
import { planLocalCallExpansion } from './local-call-expansion.js';
|
|
53
|
+
import {
|
|
54
|
+
eventShapeRuntimeEvidence,
|
|
55
|
+
outboundTraceEdgeType,
|
|
56
|
+
outboundTraceTargetNode,
|
|
57
|
+
visibleEventShapeRows,
|
|
58
|
+
} from './event-shape-candidate-trace.js';
|
|
54
59
|
|
|
55
60
|
export interface CallRow extends Record<string, unknown> {
|
|
56
61
|
id: number;
|
|
@@ -90,16 +95,6 @@ export interface TraceExecutionRuntime {
|
|
|
90
95
|
recorder: TraceEdgeRecorder;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
function traceEdgeType(call: CallRow, row: TraceGraphRow): string {
|
|
94
|
-
if (row.to_kind === 'operation'
|
|
95
|
-
&& row.edge_type === 'REMOTE_CALL_RESOLVES_TO_OPERATION')
|
|
96
|
-
return 'remote_action';
|
|
97
|
-
if (row.to_kind === 'operation'
|
|
98
|
-
&& row.edge_type === 'LOCAL_CALL_RESOLVES_TO_OPERATION')
|
|
99
|
-
return 'local_service_call';
|
|
100
|
-
return String(call.call_type);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
98
|
function includeCall(type: string, options: TraceOptions): boolean {
|
|
104
99
|
if (!options.includeDb && type === 'local_db_query') return false;
|
|
105
100
|
if (!options.includeExternal && type === 'external_http') return false;
|
|
@@ -339,7 +334,9 @@ function processOutboundCall(
|
|
|
339
334
|
const contextual = contextualRuntimeResolution(
|
|
340
335
|
runtime.db, call, bindings.get(receiver), call.workspaceId, persistedRows,
|
|
341
336
|
);
|
|
342
|
-
const rows = contextual.row
|
|
337
|
+
const rows = contextual.row
|
|
338
|
+
? [contextual.row]
|
|
339
|
+
: visibleEventShapeRows(persistedRows, runtime.options);
|
|
343
340
|
for (const row of rows)
|
|
344
341
|
processOutboundRow(runtime, current, call, { ...row }, contextual);
|
|
345
342
|
}
|
|
@@ -385,16 +382,23 @@ function recordEffectiveOutbound(
|
|
|
385
382
|
): EffectiveOutbound {
|
|
386
383
|
const persisted = parseTraceEvidence(row.evidence_json);
|
|
387
384
|
const raw = baseTraceEvidence(row, call, persisted, contextual.evidence);
|
|
388
|
-
const
|
|
385
|
+
const resolved = runtimeEventResolution(row, raw, runtime.options.vars)
|
|
389
386
|
?? runtimeResolution(runtime.db, row, raw, {
|
|
390
387
|
vars: runtime.options.vars,
|
|
391
388
|
dynamicMode: runtime.options.dynamicMode ?? 'strict',
|
|
392
389
|
maxDynamicCandidates: runtime.options.maxDynamicCandidates,
|
|
393
390
|
}, call.workspaceId, contextual.state);
|
|
391
|
+
const effective = {
|
|
392
|
+
...resolved,
|
|
393
|
+
evidence: eventShapeRuntimeEvidence(
|
|
394
|
+
runtime.db, call.workspaceId, call.id, call.call_type,
|
|
395
|
+
resolved.evidence, runtime.options.vars,
|
|
396
|
+
),
|
|
397
|
+
};
|
|
394
398
|
const target = `${effective.row.to_kind}:${effective.row.to_id}`;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
399
|
+
runtime.nodes.set(
|
|
400
|
+
target, outboundTraceTargetNode(runtime.db, target, effective.row),
|
|
401
|
+
);
|
|
398
402
|
const to = edgeTarget(effective.row, effective.evidence);
|
|
399
403
|
const edge = outboundTraceEdge(
|
|
400
404
|
current.depth, call, effective.row, to,
|
|
@@ -418,18 +422,6 @@ function recordEffectiveOutbound(
|
|
|
418
422
|
};
|
|
419
423
|
}
|
|
420
424
|
|
|
421
|
-
function targetNode(
|
|
422
|
-
id: string,
|
|
423
|
-
row: TraceGraphRow,
|
|
424
|
-
): Record<string, unknown> {
|
|
425
|
-
return {
|
|
426
|
-
id,
|
|
427
|
-
kind: row.to_kind,
|
|
428
|
-
label: row.to_kind === 'db_entity'
|
|
429
|
-
? `Entity: ${row.to_id || 'unknown'}` : row.to_id,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
425
|
function outboundTraceEdge(
|
|
434
426
|
depth: number,
|
|
435
427
|
call: CallRow,
|
|
@@ -440,7 +432,7 @@ function outboundTraceEdge(
|
|
|
440
432
|
): TraceEdge {
|
|
441
433
|
return {
|
|
442
434
|
step: depth,
|
|
443
|
-
type:
|
|
435
|
+
type: outboundTraceEdgeType(call, row),
|
|
444
436
|
from: `${call.repoName}:${call.source_file}:${call.source_line}`,
|
|
445
437
|
to,
|
|
446
438
|
evidence,
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EventSkeletonFact } from './utils/event-skeleton.js';
|
|
2
|
+
|
|
1
3
|
export type RepoKind =
|
|
2
4
|
| 'cap-service'
|
|
3
5
|
| 'cap-db-model'
|
|
@@ -36,6 +38,7 @@ export type EdgeType =
|
|
|
36
38
|
| 'HANDLER_EMITS_EVENT'
|
|
37
39
|
| 'EVENT_CONSUMED_BY_HANDLER'
|
|
38
40
|
| 'EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
41
|
+
| 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER'
|
|
39
42
|
| 'REPO_IMPORTS_HELPER_PACKAGE'
|
|
40
43
|
| 'HELPER_PACKAGE_PROVIDES_HANDLER'
|
|
41
44
|
| 'DYNAMIC_EDGE_CANDIDATE'
|
|
@@ -229,6 +232,7 @@ export interface OutboundCallFact {
|
|
|
229
232
|
operationPathExpr?: string;
|
|
230
233
|
queryEntity?: string;
|
|
231
234
|
eventNameExpr?: string;
|
|
235
|
+
eventSkeleton?: EventSkeletonFact;
|
|
232
236
|
payloadSummary?: string;
|
|
233
237
|
sourceFile: string;
|
|
234
238
|
sourceLine: number;
|
|
@@ -272,9 +276,21 @@ export interface SymbolCallFact {
|
|
|
272
276
|
}
|
|
273
277
|
export interface GeneratedConstantFact {
|
|
274
278
|
name: string;
|
|
275
|
-
value
|
|
279
|
+
value?: string;
|
|
276
280
|
sourceFile: string;
|
|
277
281
|
sourceLine: number;
|
|
282
|
+
containerName?: string;
|
|
283
|
+
memberName?: string;
|
|
284
|
+
constantKind: 'const_identifier' | 'enum_member' | 'const_object_property';
|
|
285
|
+
exported: boolean;
|
|
286
|
+
stable: boolean;
|
|
287
|
+
resolutionStatus: 'resolved' | 'refused';
|
|
288
|
+
unresolvedReason?: 'event_name_constant_member_not_string'
|
|
289
|
+
| 'event_name_constant_container_mutable';
|
|
290
|
+
declarationStartOffset: number;
|
|
291
|
+
declarationEndOffset: number;
|
|
292
|
+
valueStartOffset: number;
|
|
293
|
+
valueEndOffset: number;
|
|
278
294
|
}
|
|
279
295
|
export interface TraceStart {
|
|
280
296
|
repo?: string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { sha256Text } from './hashing.js';
|
|
2
|
+
import { scanPlaceholderStructure } from './placeholders.js';
|
|
3
|
+
import type {
|
|
4
|
+
EventEnvironmentReference,
|
|
5
|
+
} from '../parsers/event-environment-reference.js';
|
|
6
|
+
|
|
7
|
+
export const EVENT_SKELETON_SCHEMA = 'service-flow/event-skeleton@1';
|
|
8
|
+
export const EVENT_SKELETON_LITERAL_THRESHOLD = 8;
|
|
9
|
+
export const EVENT_SKELETON_TEXT_LIMIT = 512;
|
|
10
|
+
|
|
11
|
+
export interface EventSkeletonFact {
|
|
12
|
+
schema: typeof EVENT_SKELETON_SCHEMA;
|
|
13
|
+
status: 'complete' | 'malformed' | 'too_large';
|
|
14
|
+
signature: string | null;
|
|
15
|
+
literalSpans: string[];
|
|
16
|
+
holeCount: number;
|
|
17
|
+
sourceKeys: string[];
|
|
18
|
+
canonicalKeys: string[];
|
|
19
|
+
candidateEligible: boolean;
|
|
20
|
+
environmentBindings: EventEnvironmentReference[];
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
25
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
26
|
+
? value as Record<string, unknown> : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseJson(value: unknown): unknown {
|
|
30
|
+
if (typeof value !== 'string') return value;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(value) as unknown;
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
39
|
+
return Array.isArray(value) && value.every((item) =>
|
|
40
|
+
typeof item === 'string')
|
|
41
|
+
? value as string[] : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalKey(signature: string, index: number): string {
|
|
45
|
+
return `event.${signature.slice(0, 16)}.hole${index + 1}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function skeletonSignature(
|
|
49
|
+
literalSpans: readonly string[],
|
|
50
|
+
holeCount: number,
|
|
51
|
+
): string {
|
|
52
|
+
return sha256Text(JSON.stringify({ literalSpans, holeCount }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function completeSkeleton(
|
|
56
|
+
template: string,
|
|
57
|
+
): EventSkeletonFact {
|
|
58
|
+
const scan = scanPlaceholderStructure(template);
|
|
59
|
+
if (scan.status === 'malformed') return {
|
|
60
|
+
schema: EVENT_SKELETON_SCHEMA,
|
|
61
|
+
status: 'malformed',
|
|
62
|
+
signature: null,
|
|
63
|
+
literalSpans: [],
|
|
64
|
+
holeCount: 0,
|
|
65
|
+
sourceKeys: [],
|
|
66
|
+
canonicalKeys: [],
|
|
67
|
+
candidateEligible: false,
|
|
68
|
+
environmentBindings: [],
|
|
69
|
+
reason: scan.reason,
|
|
70
|
+
};
|
|
71
|
+
const literals: string[] = [];
|
|
72
|
+
let cursor = 0;
|
|
73
|
+
for (const span of scan.spans) {
|
|
74
|
+
literals.push(template.slice(cursor, span.start));
|
|
75
|
+
cursor = span.end;
|
|
76
|
+
}
|
|
77
|
+
literals.push(template.slice(cursor));
|
|
78
|
+
const signature = skeletonSignature(literals, scan.spans.length);
|
|
79
|
+
return {
|
|
80
|
+
schema: EVENT_SKELETON_SCHEMA,
|
|
81
|
+
status: 'complete',
|
|
82
|
+
signature,
|
|
83
|
+
literalSpans: literals,
|
|
84
|
+
holeCount: scan.spans.length,
|
|
85
|
+
sourceKeys: scan.spans.map((span) => span.key.trim()),
|
|
86
|
+
canonicalKeys: scan.spans.map((_, index) =>
|
|
87
|
+
canonicalKey(signature, index)),
|
|
88
|
+
candidateEligible: scan.spans.length > 0
|
|
89
|
+
&& literals.some((literal) =>
|
|
90
|
+
literal.length >= EVENT_SKELETON_LITERAL_THRESHOLD),
|
|
91
|
+
environmentBindings: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validCompleteSkeleton(
|
|
96
|
+
item: Record<string, unknown>,
|
|
97
|
+
literals: string[],
|
|
98
|
+
sourceKeys: string[],
|
|
99
|
+
canonicalKeys: string[],
|
|
100
|
+
): boolean {
|
|
101
|
+
const holes = item.holeCount;
|
|
102
|
+
const signature = item.signature;
|
|
103
|
+
if (!Number.isInteger(holes) || Number(holes) < 1
|
|
104
|
+
|| typeof signature !== 'string'
|
|
105
|
+
|| !/^[a-f0-9]{64}$/.test(signature)) return false;
|
|
106
|
+
const count = Number(holes);
|
|
107
|
+
return literals.length === count + 1
|
|
108
|
+
&& sourceKeys.length === count
|
|
109
|
+
&& canonicalKeys.length === count
|
|
110
|
+
&& sourceKeys.every((key) => key.length > 0 && key === key.trim())
|
|
111
|
+
&& canonicalKeys.every((key, index) =>
|
|
112
|
+
key === canonicalKey(signature, index))
|
|
113
|
+
&& new Set(canonicalKeys).size === canonicalKeys.length
|
|
114
|
+
&& signature === skeletonSignature(literals, count)
|
|
115
|
+
&& item.candidateEligible === literals.some((literal) =>
|
|
116
|
+
literal.length >= EVENT_SKELETON_LITERAL_THRESHOLD)
|
|
117
|
+
&& item.reason === undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validClosedSkeleton(
|
|
121
|
+
item: Record<string, unknown>,
|
|
122
|
+
literals: string[],
|
|
123
|
+
sourceKeys: string[],
|
|
124
|
+
canonicalKeys: string[],
|
|
125
|
+
): boolean {
|
|
126
|
+
const reason = item.reason;
|
|
127
|
+
if (item.signature !== null || item.holeCount !== 0
|
|
128
|
+
|| item.candidateEligible !== false
|
|
129
|
+
|| literals.length > 0 || sourceKeys.length > 0
|
|
130
|
+
|| canonicalKeys.length > 0
|
|
131
|
+
|| !Array.isArray(item.environmentBindings)
|
|
132
|
+
|| item.environmentBindings.length > 0) return false;
|
|
133
|
+
if (item.status === 'too_large')
|
|
134
|
+
return reason === 'event_skeleton_text_limit_exceeded';
|
|
135
|
+
return item.status === 'malformed'
|
|
136
|
+
&& typeof reason === 'string' && reason.length > 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function parseEventSkeletonFact(
|
|
140
|
+
value: unknown,
|
|
141
|
+
): EventSkeletonFact | undefined {
|
|
142
|
+
const item = record(parseJson(value));
|
|
143
|
+
if (!item || item.schema !== EVENT_SKELETON_SCHEMA
|
|
144
|
+
|| !['complete', 'malformed', 'too_large'].includes(String(item.status))
|
|
145
|
+
|| !Array.isArray(item.environmentBindings)) return undefined;
|
|
146
|
+
const literals = stringArray(item.literalSpans);
|
|
147
|
+
const sourceKeys = stringArray(item.sourceKeys);
|
|
148
|
+
const canonicalKeys = stringArray(item.canonicalKeys);
|
|
149
|
+
if (!literals || !sourceKeys || !canonicalKeys) return undefined;
|
|
150
|
+
const valid = item.status === 'complete'
|
|
151
|
+
? validCompleteSkeleton(item, literals, sourceKeys, canonicalKeys)
|
|
152
|
+
: validClosedSkeleton(item, literals, sourceKeys, canonicalKeys);
|
|
153
|
+
return valid ? item as unknown as EventSkeletonFact : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function deriveEventSkeleton(
|
|
157
|
+
template: string | undefined,
|
|
158
|
+
): EventSkeletonFact | undefined {
|
|
159
|
+
if (!template || !template.includes('${')) return undefined;
|
|
160
|
+
if (template.length <= EVENT_SKELETON_TEXT_LIMIT)
|
|
161
|
+
return completeSkeleton(template);
|
|
162
|
+
return {
|
|
163
|
+
schema: EVENT_SKELETON_SCHEMA,
|
|
164
|
+
status: 'too_large',
|
|
165
|
+
signature: null,
|
|
166
|
+
literalSpans: [],
|
|
167
|
+
holeCount: 0,
|
|
168
|
+
sourceKeys: [],
|
|
169
|
+
canonicalKeys: [],
|
|
170
|
+
candidateEligible: false,
|
|
171
|
+
environmentBindings: [],
|
|
172
|
+
reason: 'event_skeleton_text_limit_exceeded',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function eventTemplateVariables(
|
|
177
|
+
skeleton: EventSkeletonFact | undefined,
|
|
178
|
+
variables: Record<string, string>,
|
|
179
|
+
): Record<string, string> {
|
|
180
|
+
if (!skeleton || skeleton.status !== 'complete') return variables;
|
|
181
|
+
const expanded = { ...variables };
|
|
182
|
+
for (let index = 0; index < skeleton.sourceKeys.length; index += 1) {
|
|
183
|
+
const sourceKey = skeleton.sourceKeys[index];
|
|
184
|
+
const canonicalKeyValue = skeleton.canonicalKeys[index];
|
|
185
|
+
if (!sourceKey || !canonicalKeyValue
|
|
186
|
+
|| Object.hasOwn(expanded, sourceKey)
|
|
187
|
+
|| !Object.hasOwn(expanded, canonicalKeyValue)) continue;
|
|
188
|
+
expanded[sourceKey] = expanded[canonicalKeyValue] ?? '';
|
|
189
|
+
}
|
|
190
|
+
return expanded;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function eventMissingVariableNames(
|
|
194
|
+
skeleton: EventSkeletonFact | undefined,
|
|
195
|
+
missingSourceKeys: readonly string[],
|
|
196
|
+
): string[] {
|
|
197
|
+
const names = new Set(missingSourceKeys);
|
|
198
|
+
if (skeleton?.status === 'complete')
|
|
199
|
+
for (let index = 0; index < skeleton.sourceKeys.length; index += 1) {
|
|
200
|
+
const sourceKey = skeleton.sourceKeys[index];
|
|
201
|
+
const canonical = skeleton.canonicalKeys[index];
|
|
202
|
+
if (sourceKey && canonical && names.has(sourceKey))
|
|
203
|
+
names.add(canonical);
|
|
204
|
+
}
|
|
205
|
+
return [...names].sort((left, right) =>
|
|
206
|
+
left < right ? -1 : left > right ? 1 : 0);
|
|
207
|
+
}
|
package/src/version.ts
CHANGED