@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
|
@@ -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'
|
|
@@ -60,9 +64,23 @@ export interface EventSubscriberTransition {
|
|
|
60
64
|
omittedSymbolCallUnresolvedReasonCharacterCount?: number;
|
|
61
65
|
matchStrategy?: string;
|
|
62
66
|
dispatchCertainty?: string;
|
|
67
|
+
subscriptionConsumerRepoId?: number;
|
|
68
|
+
subscriptionConsumerRepoName?: string;
|
|
69
|
+
dispatchProvenances?: EventDispatchProvenance[];
|
|
70
|
+
dispatchProvenanceCount?: number;
|
|
71
|
+
shownDispatchProvenanceCount?: number;
|
|
72
|
+
omittedDispatchProvenanceCount?: number;
|
|
63
73
|
handler?: EventSubscriberSymbolTarget;
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
export interface EventDispatchProvenance {
|
|
77
|
+
graphEdgeId: number;
|
|
78
|
+
matchStrategy: string;
|
|
79
|
+
dispatchCertainty: string;
|
|
80
|
+
consumerRepoId?: number;
|
|
81
|
+
consumerRepoName?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
export interface EventSubscriberTransitionQuery {
|
|
67
85
|
workspaceId: number;
|
|
68
86
|
graphGeneration: number;
|
|
@@ -96,7 +114,8 @@ export function planEventSubscriberTransitions(
|
|
|
96
114
|
): PlannedEventSubscriberTransition[] {
|
|
97
115
|
return loadEventSubscriberTransitions(db, query).map((transition) => {
|
|
98
116
|
const handler = transition.handler;
|
|
99
|
-
if (
|
|
117
|
+
if (transition.status !== 'resolved' || !handler)
|
|
118
|
+
return plannedTransition(transition, 'not_resolved');
|
|
100
119
|
if (depth >= maxDepth)
|
|
101
120
|
return plannedTransition(transition, 'depth_limited');
|
|
102
121
|
const state = scheduler.schedule({
|
|
@@ -166,6 +185,10 @@ export function eventTransitionEvidence(
|
|
|
166
185
|
eventName: transition.eventName,
|
|
167
186
|
matchStrategy: transition.matchStrategy ?? 'workspace_exact_event_name',
|
|
168
187
|
dispatchCertainty: transition.dispatchCertainty ?? 'static_name_only',
|
|
188
|
+
dispatchProvenances: transition.dispatchProvenances,
|
|
189
|
+
dispatchProvenanceCount: transition.dispatchProvenanceCount,
|
|
190
|
+
shownDispatchProvenanceCount: transition.shownDispatchProvenanceCount,
|
|
191
|
+
omittedDispatchProvenanceCount: transition.omittedDispatchProvenanceCount,
|
|
169
192
|
associationBasis: transition.associationBasis,
|
|
170
193
|
dispatchScope: transition.dispatchScope,
|
|
171
194
|
roleSiteMatchCount: transition.roleSiteMatchCount,
|
|
@@ -234,24 +257,72 @@ export function loadEventSubscriberTransitions(
|
|
|
234
257
|
subscribe.call_site_start_offset,subscribe.call_site_end_offset,ge.id`).all(
|
|
235
258
|
query.workspaceId, query.graphGeneration,
|
|
236
259
|
);
|
|
237
|
-
|
|
260
|
+
const transitions = rows.flatMap((raw) => {
|
|
238
261
|
const row = eventRowForQuery(raw, query);
|
|
239
262
|
const transition = row ? transitionFromRow(row) : undefined;
|
|
240
263
|
return transition ? [transition] : [];
|
|
241
264
|
});
|
|
265
|
+
return deduplicateDispatchProvenance(transitions);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const dispatchProvenanceLimit = 5;
|
|
269
|
+
|
|
270
|
+
function transitionIdentity(value: EventSubscriberTransition): string {
|
|
271
|
+
const subscription = value.subscribeCallId ?? `edge:${value.graphEdgeId}`;
|
|
272
|
+
return `${value.eventName}\0${subscription}\0${value.targetKind}\0${
|
|
273
|
+
value.targetId}\0${value.status}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function dispatchProvenance(
|
|
277
|
+
value: EventSubscriberTransition,
|
|
278
|
+
): EventDispatchProvenance {
|
|
279
|
+
return {
|
|
280
|
+
graphEdgeId: value.graphEdgeId,
|
|
281
|
+
matchStrategy: value.matchStrategy ?? 'workspace_exact_event_name',
|
|
282
|
+
dispatchCertainty: value.dispatchCertainty ?? 'static_name_only',
|
|
283
|
+
consumerRepoId: value.subscriptionConsumerRepoId,
|
|
284
|
+
consumerRepoName: value.subscriptionConsumerRepoName,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function deduplicateDispatchProvenance(
|
|
289
|
+
values: EventSubscriberTransition[],
|
|
290
|
+
): EventSubscriberTransition[] {
|
|
291
|
+
const groups = new Map<string, EventSubscriberTransition[]>();
|
|
292
|
+
for (const value of values) {
|
|
293
|
+
const key = transitionIdentity(value);
|
|
294
|
+
groups.set(key, [...(groups.get(key) ?? []), value]);
|
|
295
|
+
}
|
|
296
|
+
return [...groups.values()].map((group) => {
|
|
297
|
+
const first = group[0];
|
|
298
|
+
if (!first || group.length === 1) return first;
|
|
299
|
+
const provenances = group.map(dispatchProvenance).slice(
|
|
300
|
+
0, dispatchProvenanceLimit,
|
|
301
|
+
);
|
|
302
|
+
return {
|
|
303
|
+
...first,
|
|
304
|
+
dispatchProvenances: provenances,
|
|
305
|
+
dispatchProvenanceCount: group.length,
|
|
306
|
+
shownDispatchProvenanceCount: provenances.length,
|
|
307
|
+
omittedDispatchProvenanceCount: group.length - provenances.length,
|
|
308
|
+
};
|
|
309
|
+
}).filter((value): value is EventSubscriberTransition =>
|
|
310
|
+
value !== undefined);
|
|
242
311
|
}
|
|
243
312
|
|
|
244
313
|
function eventRowForQuery(
|
|
245
314
|
row: Record<string, unknown>,
|
|
246
315
|
query: EventSubscriberTransitionQuery,
|
|
247
316
|
): Record<string, unknown> | undefined {
|
|
317
|
+
const exact = exactPersistedEventRow(row, query);
|
|
318
|
+
if (exact) return exact;
|
|
248
319
|
const variables = query.vars;
|
|
249
|
-
if (variables === undefined) return
|
|
320
|
+
if (variables === undefined) return undefined;
|
|
250
321
|
const evidence = parseEvidence(row.evidenceJson);
|
|
251
322
|
const resolution = isRecord(evidence.eventTemplateResolution)
|
|
252
323
|
? evidence.eventTemplateResolution : {};
|
|
253
324
|
if (typeof resolution.original !== 'string')
|
|
254
|
-
return
|
|
325
|
+
return undefined;
|
|
255
326
|
return runtimeMatchedEventRow(
|
|
256
327
|
row, query, evidence, resolution.original, variables,
|
|
257
328
|
);
|
|
@@ -272,7 +343,10 @@ function runtimeMatchedEventRow(
|
|
|
272
343
|
template: string,
|
|
273
344
|
variables: Record<string, string>,
|
|
274
345
|
): Record<string, unknown> | undefined {
|
|
275
|
-
const
|
|
346
|
+
const skeleton = parseEventSkeletonFact(evidence.eventSkeleton);
|
|
347
|
+
const substitution = substituteVariables(
|
|
348
|
+
template, eventTemplateVariables(skeleton, variables),
|
|
349
|
+
);
|
|
276
350
|
if (substitution.missing.length > 0
|
|
277
351
|
|| substitution.effective !== query.eventName) return undefined;
|
|
278
352
|
const resolvedAssociation = evidence.associationStatus === 'resolved';
|
|
@@ -295,8 +369,7 @@ export function eventSubscriberMissingVariables(
|
|
|
295
369
|
db: Db,
|
|
296
370
|
query: EventSubscriberTransitionQuery,
|
|
297
371
|
): string[] {
|
|
298
|
-
|
|
299
|
-
const variables = query.vars;
|
|
372
|
+
const variables = query.vars ?? {};
|
|
300
373
|
const rows = db.prepare(`SELECT ge.from_id eventName,
|
|
301
374
|
ge.evidence_json evidenceJson FROM graph_edges ge
|
|
302
375
|
WHERE ge.workspace_id=? AND ge.generation=?
|
|
@@ -311,7 +384,10 @@ export function eventSubscriberMissingVariables(
|
|
|
311
384
|
const template = typeof resolution.original === 'string'
|
|
312
385
|
? resolution.original : undefined;
|
|
313
386
|
if (!template) return [];
|
|
314
|
-
const
|
|
387
|
+
const skeleton = parseEventSkeletonFact(evidence.eventSkeleton);
|
|
388
|
+
const substitution = substituteVariables(
|
|
389
|
+
template, eventTemplateVariables(skeleton, variables),
|
|
390
|
+
);
|
|
315
391
|
if (substitution.missing.length === 0
|
|
316
392
|
|| matchRuntimeTemplate(
|
|
317
393
|
substitution.effective, query.eventName,
|
|
@@ -382,6 +458,12 @@ function associationEvidence(
|
|
|
382
458
|
: undefined,
|
|
383
459
|
matchStrategy: stringValue(evidence.matchStrategy),
|
|
384
460
|
dispatchCertainty: stringValue(evidence.dispatchCertainty),
|
|
461
|
+
subscriptionConsumerRepoId: numberValue(
|
|
462
|
+
evidence.subscriptionConsumerRepositoryId,
|
|
463
|
+
),
|
|
464
|
+
subscriptionConsumerRepoName: stringValue(
|
|
465
|
+
evidence.subscriptionConsumerRepositoryName,
|
|
466
|
+
),
|
|
385
467
|
};
|
|
386
468
|
}
|
|
387
469
|
|
package/src/trace/evidence.ts
CHANGED
|
@@ -23,13 +23,6 @@ export interface TraceGraphRow extends Record<string, unknown> {
|
|
|
23
23
|
status?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
interface Candidate {
|
|
27
|
-
servicePath?: string;
|
|
28
|
-
operationPath?: string;
|
|
29
|
-
repoName?: string;
|
|
30
|
-
operationName?: string;
|
|
31
|
-
score?: number;
|
|
32
|
-
}
|
|
33
26
|
interface RuntimeDiagnosticTotals {
|
|
34
27
|
missing: Set<string>;
|
|
35
28
|
candidateCount: number;
|
|
@@ -262,6 +255,8 @@ function runtimeDiagnosticTotals(
|
|
|
262
255
|
const effective = parseObject(edge.evidence.effectiveResolution);
|
|
263
256
|
if (!['dynamic', 'unresolved', 'ambiguous'].includes(String(effective.status ?? '')))
|
|
264
257
|
continue;
|
|
258
|
+
for (const key of stringArray(edge.evidence.missingRuntimeVariables))
|
|
259
|
+
missing.add(key);
|
|
265
260
|
const substitutions = edge.evidence.runtimeSubstitutions;
|
|
266
261
|
if (!substitutions || typeof substitutions !== 'object' || Array.isArray(substitutions)) continue;
|
|
267
262
|
for (const value of Object.values(substitutions as Record<string, RuntimeSubstitution>))
|
|
@@ -340,27 +335,6 @@ export function runtimeNoCandidateDiagnostics(
|
|
|
340
335
|
});
|
|
341
336
|
}
|
|
342
337
|
|
|
343
|
-
export function edgeTarget(row: TraceGraphRow, evidence: Record<string, unknown>): string {
|
|
344
|
-
const effective = parseObject(evidence.effectiveResolution);
|
|
345
|
-
const targetServicePath = stringValue(effective.targetServicePath ?? evidence.targetServicePath);
|
|
346
|
-
const targetOperationPath = stringValue(effective.targetOperationPath ?? evidence.targetOperationPath);
|
|
347
|
-
if (targetServicePath && targetOperationPath) return `${targetServicePath}${targetOperationPath}`;
|
|
348
|
-
const runtimeCandidate = evidence.runtimeResolvedCandidate as Candidate | undefined;
|
|
349
|
-
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath) return `${runtimeCandidate.servicePath}${runtimeCandidate.operationPath}`;
|
|
350
|
-
const servicePath = stringValue(evidence.servicePath);
|
|
351
|
-
const operationPath = stringValue(evidence.operationPath);
|
|
352
|
-
const targetOperation = stringValue(evidence.targetOperation);
|
|
353
|
-
const targetRepo = stringValue(evidence.targetRepo) ?? '';
|
|
354
|
-
if (row.edge_type === 'HANDLER_RUNS_DB_QUERY') return `Entity: ${row.to_id || 'unknown'}`;
|
|
355
|
-
if (row.edge_type === 'HANDLER_RUNS_REMOTE_QUERY') return stringValue(evidence.remoteQueryTarget) ?? `Remote query: ${row.to_id || 'unknown'}`;
|
|
356
|
-
if (row.edge_type === 'HANDLER_CALLS_EXTERNAL_HTTP') {
|
|
357
|
-
const target = parseObject(evidence.externalTarget);
|
|
358
|
-
return stringValue(target.label) ?? `External endpoint: ${row.to_id || 'unknown'}`;
|
|
359
|
-
}
|
|
360
|
-
if (servicePath && operationPath) return `${servicePath}${operationPath}`;
|
|
361
|
-
return targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
338
|
function persistedResolution(row: TraceGraphRow): Record<string, unknown> {
|
|
365
339
|
return {
|
|
366
340
|
status: row.status,
|
|
@@ -648,6 +622,11 @@ function stringValue(value: unknown): string | undefined {
|
|
|
648
622
|
return typeof value === 'string' ? value : undefined;
|
|
649
623
|
}
|
|
650
624
|
|
|
625
|
+
function stringArray(value: unknown): string[] {
|
|
626
|
+
return Array.isArray(value)
|
|
627
|
+
? value.filter((item): item is string => typeof item === 'string') : [];
|
|
628
|
+
}
|
|
629
|
+
|
|
651
630
|
function numeric(value: unknown): number {
|
|
652
631
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
653
632
|
}
|
|
@@ -2,10 +2,10 @@ import type { Db } from '../db/connection.js';
|
|
|
2
2
|
import type { TraceEdge, TraceOptions } from '../types.js';
|
|
3
3
|
import {
|
|
4
4
|
baseTraceEvidence,
|
|
5
|
-
edgeTarget,
|
|
6
5
|
runtimeResolution,
|
|
7
6
|
type TraceGraphRow,
|
|
8
7
|
} from './evidence.js';
|
|
8
|
+
import { edgeTarget } from './edge-target.js';
|
|
9
9
|
import { dynamicCandidateBranches } from './dynamic-branches.js';
|
|
10
10
|
import {
|
|
11
11
|
contextualRuntimeResolution,
|
|
@@ -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,23 @@ 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
|
+
| 'event_name_constant_container_unsafe_reference'
|
|
291
|
+
| 'event_name_constant_container_unsupported_shape';
|
|
292
|
+
declarationStartOffset: number;
|
|
293
|
+
declarationEndOffset: number;
|
|
294
|
+
valueStartOffset: number;
|
|
295
|
+
valueEndOffset: number;
|
|
278
296
|
}
|
|
279
297
|
export interface TraceStart {
|
|
280
298
|
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