@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
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
|
|
3
|
+
interface SourceFileSnapshot {
|
|
4
|
+
repoPath: string;
|
|
5
|
+
filePath: string;
|
|
6
|
+
text: string;
|
|
7
|
+
sizeBytes: number;
|
|
8
|
+
sourceFile: () => ts.SourceFile;
|
|
9
|
+
}
|
|
10
|
+
interface RepositorySourceContext {
|
|
11
|
+
get: (filePath: string) => SourceFileSnapshot | undefined;
|
|
12
|
+
entries: () => SourceFileSnapshot[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type EventEnvironmentTransform = 'toUpperCase' | 'toLowerCase';
|
|
16
|
+
interface EventEnvironmentReference {
|
|
17
|
+
status: 'resolved' | 'refused';
|
|
18
|
+
sourceKey: string;
|
|
19
|
+
environmentKey?: string;
|
|
20
|
+
transforms: EventEnvironmentTransform[];
|
|
21
|
+
sourceFile?: string;
|
|
22
|
+
startOffset?: number;
|
|
23
|
+
endOffset?: number;
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare const EVENT_SKELETON_SCHEMA = "service-flow/event-skeleton@1";
|
|
28
|
+
interface EventSkeletonFact {
|
|
29
|
+
schema: typeof EVENT_SKELETON_SCHEMA;
|
|
30
|
+
status: 'complete' | 'malformed' | 'too_large';
|
|
31
|
+
signature: string | null;
|
|
32
|
+
literalSpans: string[];
|
|
33
|
+
holeCount: number;
|
|
34
|
+
sourceKeys: string[];
|
|
35
|
+
canonicalKeys: string[];
|
|
36
|
+
candidateEligible: boolean;
|
|
37
|
+
environmentBindings: EventEnvironmentReference[];
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
3
41
|
type CallType = 'remote_action' | 'remote_query' | 'remote_entity_read' | 'remote_entity_mutation' | 'remote_entity_delete' | 'remote_entity_media' | 'remote_entity_candidate' | 'local_db_query' | 'external_http' | 'async_emit' | 'async_subscribe' | 'local_service_call' | 'unknown';
|
|
4
|
-
type EdgeType = 'REPO_HAS_SERVICE' | 'SERVICE_HAS_OPERATION' | 'OPERATION_IMPLEMENTED_BY_HANDLER' | 'HANDLER_REGISTERED_BY_SERVER' | 'HANDLER_CALLS_LOCAL_FUNCTION' | 'HANDLER_USES_SERVICE_ALIAS' | 'HANDLER_CALLS_REMOTE_OPERATION' | 'REMOTE_CALL_RESOLVES_TO_OPERATION' | 'LOCAL_CALL_RESOLVES_TO_OPERATION' | 'HANDLER_RUNS_DB_QUERY' | 'HANDLER_RUNS_REMOTE_QUERY' | 'HANDLER_ACCESSES_REMOTE_ENTITY' | 'HANDLER_CALLS_EXTERNAL_HTTP' | 'HANDLER_CALLS_TRANSPORT_METHOD' | 'HANDLER_EMITS_EVENT' | 'EVENT_CONSUMED_BY_HANDLER' | 'EVENT_SUBSCRIPTION_HANDLED_BY' | 'REPO_IMPORTS_HELPER_PACKAGE' | 'HELPER_PACKAGE_PROVIDES_HANDLER' | 'DYNAMIC_EDGE_CANDIDATE' | 'UNRESOLVED_EDGE';
|
|
42
|
+
type EdgeType = 'REPO_HAS_SERVICE' | 'SERVICE_HAS_OPERATION' | 'OPERATION_IMPLEMENTED_BY_HANDLER' | 'HANDLER_REGISTERED_BY_SERVER' | 'HANDLER_CALLS_LOCAL_FUNCTION' | 'HANDLER_USES_SERVICE_ALIAS' | 'HANDLER_CALLS_REMOTE_OPERATION' | 'REMOTE_CALL_RESOLVES_TO_OPERATION' | 'LOCAL_CALL_RESOLVES_TO_OPERATION' | 'HANDLER_RUNS_DB_QUERY' | 'HANDLER_RUNS_REMOTE_QUERY' | 'HANDLER_ACCESSES_REMOTE_ENTITY' | 'HANDLER_CALLS_EXTERNAL_HTTP' | 'HANDLER_CALLS_TRANSPORT_METHOD' | 'HANDLER_EMITS_EVENT' | 'EVENT_CONSUMED_BY_HANDLER' | 'EVENT_SUBSCRIPTION_HANDLED_BY' | 'EVENT_SHAPE_CANDIDATE_SUBSCRIBER' | 'REPO_IMPORTS_HELPER_PACKAGE' | 'HELPER_PACKAGE_PROVIDES_HANDLER' | 'DYNAMIC_EDGE_CANDIDATE' | 'UNRESOLVED_EDGE';
|
|
5
43
|
interface DiscoveredRepository {
|
|
6
44
|
name: string;
|
|
7
45
|
absolutePath: string;
|
|
@@ -149,6 +187,7 @@ interface OutboundCallFact {
|
|
|
149
187
|
operationPathExpr?: string;
|
|
150
188
|
queryEntity?: string;
|
|
151
189
|
eventNameExpr?: string;
|
|
190
|
+
eventSkeleton?: EventSkeletonFact;
|
|
152
191
|
payloadSummary?: string;
|
|
153
192
|
sourceFile: string;
|
|
154
193
|
sourceLine: number;
|
|
@@ -194,9 +233,20 @@ interface SymbolCallFact {
|
|
|
194
233
|
}
|
|
195
234
|
interface GeneratedConstantFact {
|
|
196
235
|
name: string;
|
|
197
|
-
value
|
|
236
|
+
value?: string;
|
|
198
237
|
sourceFile: string;
|
|
199
238
|
sourceLine: number;
|
|
239
|
+
containerName?: string;
|
|
240
|
+
memberName?: string;
|
|
241
|
+
constantKind: 'const_identifier' | 'enum_member' | 'const_object_property';
|
|
242
|
+
exported: boolean;
|
|
243
|
+
stable: boolean;
|
|
244
|
+
resolutionStatus: 'resolved' | 'refused';
|
|
245
|
+
unresolvedReason?: 'event_name_constant_member_not_string' | 'event_name_constant_container_mutable';
|
|
246
|
+
declarationStartOffset: number;
|
|
247
|
+
declarationEndOffset: number;
|
|
248
|
+
valueStartOffset: number;
|
|
249
|
+
valueEndOffset: number;
|
|
200
250
|
}
|
|
201
251
|
interface TraceStart {
|
|
202
252
|
repo?: string;
|
|
@@ -250,18 +300,6 @@ interface ParsePackageJsonOptions {
|
|
|
250
300
|
}
|
|
251
301
|
declare function parsePackageJson(repoPath: string, options?: ParsePackageJsonOptions): Promise<PackageFacts>;
|
|
252
302
|
|
|
253
|
-
interface SourceFileSnapshot {
|
|
254
|
-
repoPath: string;
|
|
255
|
-
filePath: string;
|
|
256
|
-
text: string;
|
|
257
|
-
sizeBytes: number;
|
|
258
|
-
sourceFile: () => ts.SourceFile;
|
|
259
|
-
}
|
|
260
|
-
interface RepositorySourceContext {
|
|
261
|
-
get: (filePath: string) => SourceFileSnapshot | undefined;
|
|
262
|
-
entries: () => SourceFileSnapshot[];
|
|
263
|
-
}
|
|
264
|
-
|
|
265
303
|
declare function parseCdsFile(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<CdsServiceFact[]>;
|
|
266
304
|
|
|
267
305
|
declare function parseDecorators(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<HandlerClassFact[]>;
|
|
@@ -314,6 +352,7 @@ interface LinkWorkspaceResult {
|
|
|
314
352
|
subscriptionHandlerAmbiguousCount: number;
|
|
315
353
|
subscriptionHandlerUnresolvedCount: number;
|
|
316
354
|
subscriptionHandlerMissingAssociationCount: number;
|
|
355
|
+
eventShapeCandidateCount: number;
|
|
317
356
|
}
|
|
318
357
|
declare function linkWorkspace(db: Db, workspaceId: number, vars?: Record<string, string>): LinkWorkspaceResult;
|
|
319
358
|
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
discoverRepositories,
|
|
6
6
|
extractPlaceholders,
|
|
7
7
|
linkWorkspace,
|
|
8
|
-
normalizePath,
|
|
9
8
|
parseCdsFile,
|
|
10
9
|
parseDecorators,
|
|
10
|
+
parseGeneratedConstants,
|
|
11
11
|
parseHandlerRegistrations,
|
|
12
12
|
parseImplementationHint,
|
|
13
13
|
parseOutboundCalls,
|
|
@@ -15,31 +15,10 @@ import {
|
|
|
15
15
|
parseServiceBindings,
|
|
16
16
|
redactText,
|
|
17
17
|
redactValue,
|
|
18
|
-
stripQuotes,
|
|
19
18
|
substituteVariables,
|
|
20
19
|
trace,
|
|
21
20
|
traceAndCompact
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
|
|
24
|
-
// src/parsers/generated-constants-parser.ts
|
|
25
|
-
import fs from "fs/promises";
|
|
26
|
-
import path from "path";
|
|
27
|
-
function lineOf(text, idx) {
|
|
28
|
-
return text.slice(0, idx).split("\n").length;
|
|
29
|
-
}
|
|
30
|
-
async function parseGeneratedConstants(repoPath, filePath) {
|
|
31
|
-
const text = await fs.readFile(path.join(repoPath, filePath), "utf8");
|
|
32
|
-
return [
|
|
33
|
-
...text.matchAll(
|
|
34
|
-
/(?:export\s+)?(?:const|static\s+readonly)\s+(\w+)\s*=\s*(['"])([^'"]+)\2/g
|
|
35
|
-
)
|
|
36
|
-
].map((m) => ({
|
|
37
|
-
name: m[1] ?? "constant",
|
|
38
|
-
value: stripQuotes(m[3] ?? ""),
|
|
39
|
-
sourceFile: normalizePath(filePath),
|
|
40
|
-
sourceLine: lineOf(text, m.index ?? 0)
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
21
|
+
} from "./chunk-Z6D433R5.js";
|
|
43
22
|
export {
|
|
44
23
|
applyVariables,
|
|
45
24
|
compactTrace,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
|
|
3
|
+
type Diagnostic = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
const eventNameReason = `COALESCE(
|
|
6
|
+
json_extract(c.evidence_json,'$.eventNameUnresolvedReason'),
|
|
7
|
+
CASE WHEN c.unresolved_reason GLOB 'dynamic_event_name_*'
|
|
8
|
+
OR c.unresolved_reason GLOB 'event_name_*'
|
|
9
|
+
THEN c.unresolved_reason END
|
|
10
|
+
)`;
|
|
11
|
+
|
|
12
|
+
function workspacePredicate(alias: string): string {
|
|
13
|
+
return `(? IS NULL OR ${alias}.workspace_id=?)`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function count(value: unknown): number {
|
|
17
|
+
return Number(value ?? 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unresolvedEventNameExamples(
|
|
21
|
+
db: Db,
|
|
22
|
+
workspaceId?: number,
|
|
23
|
+
): Diagnostic[] {
|
|
24
|
+
return db.prepare(`SELECT r.name repositoryName,
|
|
25
|
+
c.source_file sourceFile,c.source_line sourceLine,
|
|
26
|
+
c.event_name_expr eventName,${eventNameReason} reason
|
|
27
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
28
|
+
WHERE c.call_type='async_emit' AND ${workspacePredicate('r')}
|
|
29
|
+
AND ${eventNameReason} IS NOT NULL
|
|
30
|
+
ORDER BY r.name COLLATE BINARY,c.source_file COLLATE BINARY,
|
|
31
|
+
c.source_line,c.id LIMIT 5`).all(
|
|
32
|
+
workspaceId, workspaceId,
|
|
33
|
+
) as Diagnostic[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function eventNameResolutionQuality(
|
|
37
|
+
db: Db,
|
|
38
|
+
workspaceId?: number,
|
|
39
|
+
): Diagnostic {
|
|
40
|
+
const aggregate = db.prepare(`SELECT COUNT(*) total,
|
|
41
|
+
SUM(CASE WHEN reason IS NOT NULL THEN 1 ELSE 0 END) unresolved
|
|
42
|
+
FROM (SELECT ${eventNameReason} reason
|
|
43
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
44
|
+
WHERE c.call_type='async_emit' AND ${workspacePredicate('r')})`).get(
|
|
45
|
+
workspaceId, workspaceId,
|
|
46
|
+
);
|
|
47
|
+
const reasons = db.prepare(`SELECT ${eventNameReason} reason,COUNT(*) count
|
|
48
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
49
|
+
WHERE c.call_type='async_emit' AND ${workspacePredicate('r')}
|
|
50
|
+
AND ${eventNameReason} IS NOT NULL
|
|
51
|
+
GROUP BY reason ORDER BY count DESC,reason COLLATE BINARY LIMIT 16`).all(
|
|
52
|
+
workspaceId, workspaceId,
|
|
53
|
+
);
|
|
54
|
+
const unresolved = count(aggregate?.unresolved);
|
|
55
|
+
return {
|
|
56
|
+
severity: unresolved > 0 ? 'warning' : 'info',
|
|
57
|
+
code: 'strict_event_name_resolution_quality',
|
|
58
|
+
message: 'Event publication name-resolution aggregate',
|
|
59
|
+
publicationTotal: count(aggregate?.total),
|
|
60
|
+
unresolvedPublicationCount: unresolved,
|
|
61
|
+
reasonBuckets: reasons,
|
|
62
|
+
examples: unresolvedEventNameExamples(db, workspaceId),
|
|
63
|
+
exampleCount: unresolved,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function dynamicEventExamples(
|
|
68
|
+
db: Db,
|
|
69
|
+
workspaceId?: number,
|
|
70
|
+
): Diagnostic[] {
|
|
71
|
+
return db.prepare(`SELECT r.name repositoryName,c.call_type callType,
|
|
72
|
+
c.source_file sourceFile,c.source_line sourceLine,
|
|
73
|
+
e.to_kind targetKind,e.to_id targetId,e.unresolved_reason reason
|
|
74
|
+
FROM graph_edges e JOIN outbound_calls c
|
|
75
|
+
ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER)
|
|
76
|
+
JOIN repositories r ON r.id=c.repo_id
|
|
77
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
78
|
+
AND ${workspacePredicate('r')}
|
|
79
|
+
AND (e.to_kind='event_candidate'
|
|
80
|
+
OR e.edge_type='DYNAMIC_EDGE_CANDIDATE')
|
|
81
|
+
ORDER BY r.name COLLATE BINARY,c.source_file COLLATE BINARY,
|
|
82
|
+
c.source_line,c.id LIMIT 5`).all(
|
|
83
|
+
workspaceId, workspaceId,
|
|
84
|
+
) as Diagnostic[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function dynamicEventQuality(
|
|
88
|
+
db: Db,
|
|
89
|
+
workspaceId?: number,
|
|
90
|
+
): Diagnostic {
|
|
91
|
+
const row = db.prepare(`SELECT
|
|
92
|
+
COUNT(DISTINCT CASE WHEN e.to_kind='event_candidate'
|
|
93
|
+
THEN e.workspace_id || ':' || e.to_id END) eventCandidateNodeCount,
|
|
94
|
+
COUNT(*) dynamicEventEdgeCount,
|
|
95
|
+
SUM(CASE WHEN json_array_length(e.evidence_json,
|
|
96
|
+
'$.eventTemplateResolution.placeholders')>0 THEN 1 ELSE 0 END)
|
|
97
|
+
variableRecoverableCount
|
|
98
|
+
FROM graph_edges e JOIN outbound_calls c
|
|
99
|
+
ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER)
|
|
100
|
+
JOIN repositories r ON r.id=c.repo_id
|
|
101
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
102
|
+
AND ${workspacePredicate('r')}
|
|
103
|
+
AND (e.to_kind='event_candidate'
|
|
104
|
+
OR e.edge_type='DYNAMIC_EDGE_CANDIDATE')`).get(
|
|
105
|
+
workspaceId, workspaceId,
|
|
106
|
+
);
|
|
107
|
+
const total = count(row?.dynamicEventEdgeCount);
|
|
108
|
+
const recoverable = count(row?.variableRecoverableCount);
|
|
109
|
+
return {
|
|
110
|
+
severity: total > 0 ? 'warning' : 'info',
|
|
111
|
+
code: 'strict_event_dynamic_candidate_quality',
|
|
112
|
+
message: 'Dynamic event candidate aggregate',
|
|
113
|
+
eventCandidateNodeCount: count(row?.eventCandidateNodeCount),
|
|
114
|
+
dynamicEventEdgeCount: total,
|
|
115
|
+
eventCandidateCount: total,
|
|
116
|
+
variableRecoverableCount: recoverable,
|
|
117
|
+
nonVariableRecoverableCount: Math.max(0, total - recoverable),
|
|
118
|
+
examples: dynamicEventExamples(db, workspaceId),
|
|
119
|
+
exampleCount: total,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function unmatchedPublicationRows(
|
|
124
|
+
db: Db,
|
|
125
|
+
workspaceId?: number,
|
|
126
|
+
): Diagnostic[] {
|
|
127
|
+
return db.prepare(`SELECT r.name repositoryName,c.source_file sourceFile,
|
|
128
|
+
c.source_line sourceLine,e.to_id eventName
|
|
129
|
+
FROM graph_edges e JOIN outbound_calls c
|
|
130
|
+
ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER)
|
|
131
|
+
JOIN repositories r ON r.id=c.repo_id
|
|
132
|
+
WHERE c.call_type='async_emit' AND e.edge_type='HANDLER_EMITS_EVENT'
|
|
133
|
+
AND e.to_kind='event' AND ${workspacePredicate('r')}
|
|
134
|
+
AND NOT EXISTS (SELECT 1 FROM graph_edges subscription
|
|
135
|
+
WHERE subscription.workspace_id=e.workspace_id
|
|
136
|
+
AND subscription.generation=e.generation
|
|
137
|
+
AND subscription.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
138
|
+
AND subscription.from_kind='event'
|
|
139
|
+
AND subscription.from_id=e.to_id)
|
|
140
|
+
ORDER BY r.name COLLATE BINARY,c.source_file COLLATE BINARY,
|
|
141
|
+
c.source_line,c.id LIMIT 5`).all(
|
|
142
|
+
workspaceId, workspaceId,
|
|
143
|
+
) as Diagnostic[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function unmatchedPublicationQuality(
|
|
147
|
+
db: Db,
|
|
148
|
+
workspaceId?: number,
|
|
149
|
+
): Diagnostic {
|
|
150
|
+
const row = db.prepare(`SELECT COUNT(*) count
|
|
151
|
+
FROM graph_edges e JOIN outbound_calls c
|
|
152
|
+
ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER)
|
|
153
|
+
JOIN repositories r ON r.id=c.repo_id
|
|
154
|
+
WHERE c.call_type='async_emit' AND e.edge_type='HANDLER_EMITS_EVENT'
|
|
155
|
+
AND e.to_kind='event' AND ${workspacePredicate('r')}
|
|
156
|
+
AND NOT EXISTS (SELECT 1 FROM graph_edges subscription
|
|
157
|
+
WHERE subscription.workspace_id=e.workspace_id
|
|
158
|
+
AND subscription.generation=e.generation
|
|
159
|
+
AND subscription.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
160
|
+
AND subscription.from_kind='event'
|
|
161
|
+
AND subscription.from_id=e.to_id)`).get(workspaceId, workspaceId);
|
|
162
|
+
const total = count(row?.count);
|
|
163
|
+
return {
|
|
164
|
+
severity: total > 0 ? 'warning' : 'info',
|
|
165
|
+
code: 'strict_event_publication_without_subscription_quality',
|
|
166
|
+
message: 'Resolved event publications without a matching subscription',
|
|
167
|
+
unmatchedPublicationCount: total,
|
|
168
|
+
examples: unmatchedPublicationRows(db, workspaceId),
|
|
169
|
+
exampleCount: total,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unmatchedSubscriptionRows(
|
|
174
|
+
db: Db,
|
|
175
|
+
workspaceId?: number,
|
|
176
|
+
): Diagnostic[] {
|
|
177
|
+
return db.prepare(`SELECT COALESCE(json_extract(e.evidence_json,
|
|
178
|
+
'$.subscriptionConsumerRepositoryName'),
|
|
179
|
+
json_extract(e.evidence_json,'$.repositoryName')) repositoryName,
|
|
180
|
+
json_extract(e.evidence_json,'$.sourceFile') sourceFile,
|
|
181
|
+
json_extract(e.evidence_json,'$.sourceLine') sourceLine,
|
|
182
|
+
e.from_id eventName,
|
|
183
|
+
json_extract(c.evidence_json,'$.subscriptionLoopRegistrationStatus')
|
|
184
|
+
loopRegistrationStatus,
|
|
185
|
+
json_extract(c.evidence_json,'$.subscriptionLoopRegistrationCount')
|
|
186
|
+
loopRegistrationCount
|
|
187
|
+
FROM graph_edges e LEFT JOIN outbound_calls c
|
|
188
|
+
ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
189
|
+
WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
190
|
+
AND ${workspacePredicate('e')}
|
|
191
|
+
AND NOT EXISTS (SELECT 1 FROM graph_edges publication
|
|
192
|
+
WHERE publication.workspace_id=e.workspace_id
|
|
193
|
+
AND publication.generation=e.generation
|
|
194
|
+
AND publication.edge_type='HANDLER_EMITS_EVENT'
|
|
195
|
+
AND publication.to_kind='event'
|
|
196
|
+
AND publication.to_id=e.from_id)
|
|
197
|
+
ORDER BY repositoryName COLLATE BINARY,sourceFile COLLATE BINARY,
|
|
198
|
+
sourceLine,e.id LIMIT 5`).all(
|
|
199
|
+
workspaceId, workspaceId,
|
|
200
|
+
) as Diagnostic[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function unmatchedSubscriptionQuality(
|
|
204
|
+
db: Db,
|
|
205
|
+
workspaceId?: number,
|
|
206
|
+
): Diagnostic {
|
|
207
|
+
const row = db.prepare(`SELECT COUNT(*) siteCount,
|
|
208
|
+
SUM(CASE
|
|
209
|
+
WHEN json_extract(c.evidence_json,
|
|
210
|
+
'$.subscriptionLoopRegistrationStatus')='enumerated'
|
|
211
|
+
THEN CAST(json_extract(c.evidence_json,
|
|
212
|
+
'$.subscriptionLoopRegistrationCount') AS INTEGER)
|
|
213
|
+
WHEN json_extract(c.evidence_json,
|
|
214
|
+
'$.subscriptionLoopRegistrationStatus')='unresolved' THEN 0
|
|
215
|
+
ELSE 1 END) count,
|
|
216
|
+
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
217
|
+
'$.subscriptionLoopRegistrationStatus')='unresolved'
|
|
218
|
+
THEN 1 ELSE 0 END) unknownMultiplicitySiteCount
|
|
219
|
+
FROM graph_edges e LEFT JOIN outbound_calls c
|
|
220
|
+
ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
221
|
+
WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
222
|
+
AND ${workspacePredicate('e')}
|
|
223
|
+
AND NOT EXISTS (SELECT 1 FROM graph_edges publication
|
|
224
|
+
WHERE publication.workspace_id=e.workspace_id
|
|
225
|
+
AND publication.generation=e.generation
|
|
226
|
+
AND publication.edge_type='HANDLER_EMITS_EVENT'
|
|
227
|
+
AND publication.to_kind='event'
|
|
228
|
+
AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
|
|
229
|
+
const total = count(row?.count);
|
|
230
|
+
const siteCount = count(row?.siteCount);
|
|
231
|
+
return {
|
|
232
|
+
severity: siteCount > 0 ? 'warning' : 'info',
|
|
233
|
+
code: 'strict_event_subscription_without_publication_quality',
|
|
234
|
+
message: 'Event subscriptions without a matching publication',
|
|
235
|
+
unmatchedSubscriptionCount: total,
|
|
236
|
+
unmatchedSubscriptionSiteCount: siteCount,
|
|
237
|
+
unknownMultiplicitySiteCount: count(row?.unknownMultiplicitySiteCount),
|
|
238
|
+
examples: unmatchedSubscriptionRows(db, workspaceId),
|
|
239
|
+
exampleCount: siteCount,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function receiverProofQuality(
|
|
244
|
+
db: Db,
|
|
245
|
+
workspaceId?: number,
|
|
246
|
+
): Diagnostic {
|
|
247
|
+
const row = db.prepare(`SELECT COUNT(*) eventTotal,
|
|
248
|
+
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
249
|
+
'$.receiverClassification')='cap_evidence' THEN 1 ELSE 0 END) proven,
|
|
250
|
+
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
251
|
+
'$.receiverClassification')='name_fallback' THEN 1 ELSE 0 END)
|
|
252
|
+
nameFallback,
|
|
253
|
+
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
254
|
+
'$.receiverClassification')='unproven' THEN 1 ELSE 0 END) unproven
|
|
255
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
256
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
257
|
+
AND ${workspacePredicate('r')}`).get(workspaceId, workspaceId);
|
|
258
|
+
const buckets = db.prepare(`SELECT CASE
|
|
259
|
+
WHEN json_extract(c.evidence_json,
|
|
260
|
+
'$.receiverClassification')='name_fallback' THEN 'name_fallback'
|
|
261
|
+
WHEN json_extract(c.evidence_json,
|
|
262
|
+
'$.receiverClassification')='unproven'
|
|
263
|
+
THEN COALESCE(c.unresolved_reason,'unproven')
|
|
264
|
+
ELSE 'missing' END reason,COUNT(*) count
|
|
265
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
266
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
267
|
+
AND ${workspacePredicate('r')}
|
|
268
|
+
AND json_extract(c.evidence_json,'$.receiverClassification')
|
|
269
|
+
<>'cap_evidence'
|
|
270
|
+
GROUP BY reason ORDER BY count DESC,reason COLLATE BINARY LIMIT 16`).all(
|
|
271
|
+
workspaceId, workspaceId,
|
|
272
|
+
);
|
|
273
|
+
const questionable = count(row?.nameFallback) + count(row?.unproven);
|
|
274
|
+
return {
|
|
275
|
+
severity: questionable > 0 ? 'warning' : 'info',
|
|
276
|
+
code: 'strict_event_receiver_classification_quality',
|
|
277
|
+
message: 'CAP event receiver proof aggregate',
|
|
278
|
+
eventTotal: count(row?.eventTotal),
|
|
279
|
+
proven: count(row?.proven),
|
|
280
|
+
nameFallback: count(row?.nameFallback),
|
|
281
|
+
unproven: count(row?.unproven),
|
|
282
|
+
questionable,
|
|
283
|
+
reasonBuckets: buckets,
|
|
284
|
+
examples: receiverProofExamples(db, workspaceId),
|
|
285
|
+
exampleCount: questionable,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function receiverProofExamples(
|
|
290
|
+
db: Db,
|
|
291
|
+
workspaceId?: number,
|
|
292
|
+
): Diagnostic[] {
|
|
293
|
+
return db.prepare(`SELECT r.name repositoryName,c.call_type callType,
|
|
294
|
+
c.source_file sourceFile,c.source_line sourceLine,
|
|
295
|
+
json_extract(c.evidence_json,'$.receiverClassification')
|
|
296
|
+
receiverClassification,
|
|
297
|
+
c.unresolved_reason reason
|
|
298
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
299
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
300
|
+
AND ${workspacePredicate('r')}
|
|
301
|
+
AND json_extract(c.evidence_json,'$.receiverClassification')
|
|
302
|
+
<>'cap_evidence'
|
|
303
|
+
ORDER BY r.name COLLATE BINARY,c.source_file COLLATE BINARY,
|
|
304
|
+
c.source_line,c.id LIMIT 5`).all(
|
|
305
|
+
workspaceId, workspaceId,
|
|
306
|
+
) as Diagnostic[];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function shapeEnvironmentExamples(
|
|
310
|
+
db: Db,
|
|
311
|
+
workspaceId?: number,
|
|
312
|
+
): Diagnostic[] {
|
|
313
|
+
return db.prepare(`SELECT e.edge_type edgeType,e.from_id fromId,
|
|
314
|
+
e.to_kind toKind,e.to_id toId,
|
|
315
|
+
json_extract(e.evidence_json,
|
|
316
|
+
'$.eventEnvironmentResolution.status') environmentStatus,
|
|
317
|
+
json_extract(e.evidence_json,
|
|
318
|
+
'$.eventEnvironmentResolution.collisionCount') collisionCount
|
|
319
|
+
FROM graph_edges e WHERE ${workspacePredicate('e')}
|
|
320
|
+
AND (e.edge_type='EVENT_SHAPE_CANDIDATE_SUBSCRIBER'
|
|
321
|
+
OR json_extract(e.evidence_json,
|
|
322
|
+
'$.eventEnvironmentResolution.status')='ambiguous'
|
|
323
|
+
OR CAST(json_extract(e.evidence_json,
|
|
324
|
+
'$.eventEnvironmentResolution.collisionCount') AS INTEGER)>1)
|
|
325
|
+
ORDER BY e.edge_type COLLATE BINARY,e.from_id COLLATE BINARY,
|
|
326
|
+
e.to_id COLLATE BINARY,e.id LIMIT 5`).all(
|
|
327
|
+
workspaceId, workspaceId,
|
|
328
|
+
) as Diagnostic[];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function shapeEnvironmentQuality(
|
|
332
|
+
db: Db,
|
|
333
|
+
workspaceId?: number,
|
|
334
|
+
): Diagnostic {
|
|
335
|
+
const row = db.prepare(`SELECT
|
|
336
|
+
SUM(CASE WHEN e.edge_type='EVENT_SHAPE_CANDIDATE_SUBSCRIBER'
|
|
337
|
+
THEN 1 ELSE 0 END) shapeCandidates,
|
|
338
|
+
SUM(CASE WHEN json_extract(e.evidence_json,
|
|
339
|
+
'$.eventEnvironmentResolution.status')='ambiguous'
|
|
340
|
+
OR CAST(json_extract(e.evidence_json,
|
|
341
|
+
'$.eventEnvironmentResolution.collisionCount') AS INTEGER)>1
|
|
342
|
+
THEN 1 ELSE 0 END) environmentAmbiguities
|
|
343
|
+
FROM graph_edges e WHERE ${workspacePredicate('e')}`).get(
|
|
344
|
+
workspaceId, workspaceId,
|
|
345
|
+
);
|
|
346
|
+
const shapes = count(row?.shapeCandidates);
|
|
347
|
+
const ambiguous = count(row?.environmentAmbiguities);
|
|
348
|
+
return {
|
|
349
|
+
severity: shapes + ambiguous > 0 ? 'warning' : 'info',
|
|
350
|
+
code: 'strict_event_shape_environment_quality',
|
|
351
|
+
message: 'Event skeleton candidate and environment ambiguity aggregate',
|
|
352
|
+
skeletonCandidateCount: shapes,
|
|
353
|
+
environmentBindingAmbiguityCount: ambiguous,
|
|
354
|
+
examples: shapeEnvironmentExamples(db, workspaceId),
|
|
355
|
+
exampleCount: shapes + ambiguous,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function eventSurfaceQualityDiagnostics(
|
|
360
|
+
db: Db,
|
|
361
|
+
workspaceId?: number,
|
|
362
|
+
): Diagnostic[] {
|
|
363
|
+
return [
|
|
364
|
+
eventNameResolutionQuality(db, workspaceId),
|
|
365
|
+
dynamicEventQuality(db, workspaceId),
|
|
366
|
+
unmatchedPublicationQuality(db, workspaceId),
|
|
367
|
+
unmatchedSubscriptionQuality(db, workspaceId),
|
|
368
|
+
receiverProofQuality(db, workspaceId),
|
|
369
|
+
shapeEnvironmentQuality(db, workspaceId),
|
|
370
|
+
];
|
|
371
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
packagePendingDiagnostics,
|
|
12
12
|
symbolCallQuality,
|
|
13
13
|
} from './doctor-package-resolution.js';
|
|
14
|
+
import {
|
|
15
|
+
eventSurfaceQualityDiagnostics,
|
|
16
|
+
} from './doctor-event-quality.js';
|
|
14
17
|
export { linkUpgradeWarnings } from './doctor-lifecycle.js';
|
|
15
18
|
|
|
16
19
|
type Diagnostic = Record<string, unknown>;
|
|
@@ -163,7 +166,7 @@ function parserQualityDiagnostics(db: Db, strict: boolean, options: DoctorOption
|
|
|
163
166
|
externalHttpTargetQuality(db),
|
|
164
167
|
odataInvocationResolutionQuality(db),
|
|
165
168
|
...jsonEvidenceQuality(db),
|
|
166
|
-
|
|
169
|
+
...eventSurfaceQualityDiagnostics(db, options.workspaceId),
|
|
167
170
|
graphDynamicFlagQuality(db),
|
|
168
171
|
symbol,
|
|
169
172
|
dbq,
|
|
@@ -202,11 +205,6 @@ function outboundOwnershipQuality(db: Db): Diagnostic {
|
|
|
202
205
|
return { severity: ratio > 0.01 ? 'warning' : 'info', code: 'strict_outbound_source_ownership_quality', message: 'Outbound call source-symbol ownership aggregate', total, withoutOwnership: without, withoutOwnershipRatio: ratio, withoutOwnershipRatioThreshold: 0.01, ownerlessByType: byType, ownerlessExamples: examples };
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
function eventReceiverQuality(db: Db): Diagnostic {
|
|
206
|
-
const row = db.prepare("SELECT SUM(CASE WHEN call_type IN ('async_emit','async_subscribe') THEN 1 ELSE 0 END) eventTotal, SUM(CASE WHEN call_type IN ('async_emit','async_subscribe') AND (json_extract(evidence_json,'$.receiverClassification') IS NULL OR json_extract(evidence_json,'$.receiverClassification') <> 'cap_evidence') THEN 1 ELSE 0 END) questionable FROM outbound_calls").get() as Record<string, unknown>;
|
|
207
|
-
return { severity: Number(row.questionable ?? 0) > 0 ? 'warning' : 'info', code: 'strict_event_receiver_classification_quality', message: 'CAP event receiver classification aggregate', eventTotal: Number(row.eventTotal ?? 0), questionable: Number(row.questionable ?? 0) };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
208
|
function graphDynamicFlagQuality(db: Db): Diagnostic {
|
|
211
209
|
const row = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get() as Record<string, unknown>;
|
|
212
210
|
return { severity: Number(row.count ?? 0) > 0 ? 'warning' : 'info', code: 'strict_graph_dynamic_flag_consistency', message: 'Graph dynamic flag consistency aggregate', dynamicTerminalEdges: Number(row.count ?? 0) };
|
package/src/cli.ts
CHANGED
|
@@ -298,7 +298,7 @@ function registerLinkCommand(program: Command): void {
|
|
|
298
298
|
const r = linkWorkspace(db, workspaceId);
|
|
299
299
|
const upgradeWarnings = linkUpgradeWarnings(db, workspaceId);
|
|
300
300
|
writeStdout(
|
|
301
|
-
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing\n`,
|
|
301
|
+
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing, ${r.eventShapeCandidateCount} event shape candidates\n`,
|
|
302
302
|
);
|
|
303
303
|
}).catch(fail),
|
|
304
304
|
);
|
|
@@ -400,11 +400,12 @@ export function insertCalls(
|
|
|
400
400
|
function outboundCallInsertStatement(db: Db): Statement {
|
|
401
401
|
return db.prepare(`INSERT INTO outbound_calls(
|
|
402
402
|
repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,
|
|
403
|
-
event_name_expr,
|
|
403
|
+
event_name_expr,event_skeleton_signature,event_skeleton_json,
|
|
404
|
+
payload_summary,source_file,source_line,call_site_start_offset,
|
|
404
405
|
call_site_end_offset,confidence,unresolved_reason,local_service_name,
|
|
405
406
|
local_service_lookup,alias_chain_json,evidence_json,external_target_kind,
|
|
406
407
|
external_target_id,external_target_label,external_target_dynamic,service_binding_id
|
|
407
|
-
) VALUES(
|
|
408
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
function insertOutboundCall(
|
|
@@ -424,7 +425,9 @@ function insertOutboundCall(
|
|
|
424
425
|
stmt.run(
|
|
425
426
|
repoId, sourceSymbolId,
|
|
426
427
|
call.callType, call.method, call.operationPathExpr, call.queryEntity,
|
|
427
|
-
call.eventNameExpr, call.
|
|
428
|
+
call.eventNameExpr, call.eventSkeleton?.signature ?? null,
|
|
429
|
+
call.eventSkeleton ? JSON.stringify(call.eventSkeleton) : null,
|
|
430
|
+
call.payloadSummary, call.sourceFile, call.sourceLine,
|
|
428
431
|
call.callSiteStartOffset, call.callSiteEndOffset, call.confidence,
|
|
429
432
|
call.unresolvedReason,
|
|
430
433
|
call.localServiceName, call.localServiceLookup,
|
|
@@ -10,9 +10,8 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
invalidBindingFactCategories,
|
|
12
12
|
} from './binding-fact-semantics.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
} from './symbol-call-semantics.js';
|
|
13
|
+
import { invalidSymbolFactCategories } from './symbol-call-semantics.js';
|
|
14
|
+
import { invalidEventFactCategories } from './event-fact-semantics.js';
|
|
16
15
|
|
|
17
16
|
export type PackageFactPhase = 'pre_package' | 'terminal';
|
|
18
17
|
|
|
@@ -262,7 +261,7 @@ function eventNameCategories(
|
|
|
262
261
|
db: Db,
|
|
263
262
|
workspaceId?: number,
|
|
264
263
|
): FactSemanticCategoryCount[] {
|
|
265
|
-
const
|
|
264
|
+
const invalidName = count(db, `SELECT COUNT(*) count
|
|
266
265
|
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
267
266
|
WHERE ${currentRepositoryPredicate()}
|
|
268
267
|
AND fact.call_type IN ('async_emit','async_subscribe')
|
|
@@ -278,7 +277,7 @@ function eventNameCategories(
|
|
|
278
277
|
fact.call_site_end_offset HAVING COUNT(*)<>1
|
|
279
278
|
)`, workspaceId);
|
|
280
279
|
return [
|
|
281
|
-
...category('event_name_invalid',
|
|
280
|
+
...category('event_name_invalid', invalidName),
|
|
282
281
|
...category('async_subscription_site_duplicate', duplicate),
|
|
283
282
|
];
|
|
284
283
|
}
|
|
@@ -686,6 +685,7 @@ export function invalidFactSemanticCategories(
|
|
|
686
685
|
...callSpanCategories(db, workspaceId),
|
|
687
686
|
...duplicateSymbolCallCategories(db, workspaceId),
|
|
688
687
|
...eventNameCategories(db, workspaceId),
|
|
688
|
+
...invalidEventFactCategories(db, workspaceId, phase),
|
|
689
689
|
...ownerCategories(db, workspaceId),
|
|
690
690
|
...bindingCategories(db, workspaceId),
|
|
691
691
|
...invalidBindingFactCategories(db, workspaceId),
|