@saptools/service-flow 0.1.72 → 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 +8 -0
- package/README.md +19 -6
- package/TECHNICAL-NOTE.md +11 -2
- package/dist/{chunk-Z6D433R5.js → chunk-32WOTGTS.js} +6714 -6161
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +189 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +90 -15
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/current-fact-semantics.ts +2 -26
- package/src/db/event-fact-semantics.ts +260 -61
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +4 -1
- package/src/db/package-target-invalidation.ts +4 -3
- package/src/db/schema.ts +1 -1
- package/src/indexer/repository-indexer.ts +50 -6
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +4 -2
- package/src/linker/event-shape-candidate-linker.ts +63 -20
- package/src/linker/event-subscription-handler-linker.ts +121 -30
- package/src/linker/package-event-constant-resolver.ts +22 -18
- package/src/output/repository-inspection.ts +11 -0
- package/src/parsers/environment-declarations.ts +104 -27
- package/src/parsers/event-call-analysis.ts +163 -35
- package/src/parsers/event-environment-reference.ts +18 -7
- package/src/parsers/event-receiver-analysis.ts +17 -27
- package/src/parsers/outbound-call-classifier.ts +1 -1
- package/src/parsers/stable-local-value.ts +12 -1
- package/src/parsers/string-constant-lookups.ts +78 -28
- package/src/trace/edge-target.ts +65 -0
- package/src/trace/event-subscriber-traversal.ts +71 -1
- package/src/trace/evidence.ts +0 -28
- package/src/trace/trace-scope-execution.ts +1 -1
- package/src/types.ts +3 -1
- package/src/version.ts +1 -1
- package/dist/chunk-Z6D433R5.js.map +0 -1
|
@@ -161,9 +161,10 @@ function resetPackageEventCalls(
|
|
|
161
161
|
const source = call.evidence.eventNameConstantSourceExpression;
|
|
162
162
|
if (typeof source !== 'string' || source.length === 0)
|
|
163
163
|
throw new Error('invalid_current_package_event_constant_evidence');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
update.run(
|
|
165
|
+
source, 'event_name_constant_resolution_pending',
|
|
166
|
+
pendingEventEvidence(call.evidence), call.id,
|
|
167
|
+
);
|
|
167
168
|
batch.affectedCallerRepoIds.add(call.repoId);
|
|
168
169
|
matched = true;
|
|
169
170
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const schemaTablesSql = `
|
|
2
2
|
CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
3
|
-
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', package_public_surface_json TEXT, environment_declarations_json TEXT DEFAULT '{"schema":"service-flow/environment-declarations@1","status":"not_applicable","reason":null,"recordCap":32,"total":0,"shown":0,"omitted":0,"declarations":[]}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
3
|
+
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', package_public_surface_json TEXT, environment_declarations_json TEXT DEFAULT '{"schema":"service-flow/environment-declarations@1","allowedKeys":["SHARD_CODE"],"status":"not_applicable","reason":null,"recordCap":32,"total":0,"shown":0,"omitted":0,"declarations":[]}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
4
4
|
CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
5
5
|
CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
6
6
|
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, extension_local_ref TEXT, extension_imported_symbol TEXT, extension_local_alias TEXT, extension_module_specifier TEXT, extension_import_kind TEXT, extension_base_service_id INTEGER, extension_base_status TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(extension_base_service_id) REFERENCES cds_services(id) ON DELETE SET NULL);
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
import {
|
|
57
57
|
loadRepositorySourceContext,
|
|
58
58
|
type RepositorySourceContext,
|
|
59
|
+
type SourceFileSnapshot,
|
|
59
60
|
type SourceContextInstrumentation,
|
|
60
61
|
} from '../parsers/ts-project.js';
|
|
61
62
|
import {
|
|
@@ -128,6 +129,7 @@ export async function prepareRepositoryIndex(
|
|
|
128
129
|
repo: RepoRow,
|
|
129
130
|
force: boolean,
|
|
130
131
|
instrumentation?: SourceContextInstrumentation,
|
|
132
|
+
eventEnvironmentKeys?: readonly string[],
|
|
131
133
|
): Promise<PreparedRepositoryIndex> {
|
|
132
134
|
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
133
135
|
const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
|
|
@@ -138,11 +140,17 @@ export async function prepareRepositoryIndex(
|
|
|
138
140
|
const sources = await loadRepositorySourceContext(
|
|
139
141
|
repo.absolute_path, sourceFiles, instrumentation,
|
|
140
142
|
);
|
|
143
|
+
const environmentDeclarations = collectEnvironmentDeclarations(
|
|
144
|
+
sources, eventEnvironmentKeys,
|
|
145
|
+
);
|
|
141
146
|
const fingerprint = repositoryFingerprint(
|
|
142
|
-
sources, packageFacts, packageSnapshot.rawText,
|
|
147
|
+
sources, packageFacts, packageSnapshot.rawText, environmentDeclarations,
|
|
143
148
|
);
|
|
144
149
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
145
|
-
const parsedFacts = await parseAllSourceFacts(
|
|
150
|
+
const parsedFacts = await parseAllSourceFacts(
|
|
151
|
+
repo.absolute_path, sources, environmentDeclarations,
|
|
152
|
+
eventEnvironmentKeys,
|
|
153
|
+
);
|
|
146
154
|
const packageSurface = analyzeRepositoryPackageSurface(
|
|
147
155
|
packageFacts, packageSnapshot.manifest, sources,
|
|
148
156
|
);
|
|
@@ -157,7 +165,7 @@ export async function prepareRepositoryIndex(
|
|
|
157
165
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
158
166
|
parsed,
|
|
159
167
|
packagePublicSurface: packageSurface.surface,
|
|
160
|
-
environmentDeclarations
|
|
168
|
+
environmentDeclarations,
|
|
161
169
|
fileCount: sourceFiles.length,
|
|
162
170
|
diagnosticCount: parsed.handlers.filter((handler) =>
|
|
163
171
|
handler.hasHandlerDecorator
|
|
@@ -269,11 +277,18 @@ export function recordIndexFailure(db: Db, repoId: number, error: unknown): void
|
|
|
269
277
|
async function parseAllSourceFacts(
|
|
270
278
|
root: string,
|
|
271
279
|
sources: RepositorySourceContext,
|
|
280
|
+
environmentDeclarations: EnvironmentDeclarationsFact,
|
|
281
|
+
eventEnvironmentKeys?: readonly string[],
|
|
272
282
|
): Promise<ParsedFacts> {
|
|
273
283
|
const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], generatedConstants: [], fileRecords: [] };
|
|
274
284
|
for (const snapshot of sources.entries()) {
|
|
275
285
|
const file = snapshot.filePath;
|
|
276
|
-
facts.fileRecords.push({
|
|
286
|
+
facts.fileRecords.push({
|
|
287
|
+
relativePath: normalizePath(file),
|
|
288
|
+
extension: path.extname(file),
|
|
289
|
+
sha256: sourceFactHash(snapshot, environmentDeclarations),
|
|
290
|
+
sizeBytes: snapshot.sizeBytes,
|
|
291
|
+
});
|
|
277
292
|
if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
|
|
278
293
|
if (/\.[jt]s$/.test(file)) {
|
|
279
294
|
const source = snapshot.sourceFile();
|
|
@@ -283,7 +298,9 @@ async function parseAllSourceFacts(
|
|
|
283
298
|
sources, source, file,
|
|
284
299
|
),
|
|
285
300
|
eventEnvironmentReferenceResolver:
|
|
286
|
-
createEventEnvironmentReferenceResolver(
|
|
301
|
+
createEventEnvironmentReferenceResolver(
|
|
302
|
+
sources, source, file, eventEnvironmentKeys,
|
|
303
|
+
),
|
|
287
304
|
});
|
|
288
305
|
facts.handlers.push(...(await parseDecorators(root, file, sources)));
|
|
289
306
|
facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
|
|
@@ -334,6 +351,7 @@ function repositoryFingerprint(
|
|
|
334
351
|
sources: RepositorySourceContext,
|
|
335
352
|
facts: PackageFacts,
|
|
336
353
|
packageJsonText: string,
|
|
354
|
+
environmentDeclarations: EnvironmentDeclarationsFact,
|
|
337
355
|
): string {
|
|
338
356
|
const normalizedFacts = {
|
|
339
357
|
analyzerVersion: ANALYZER_VERSION,
|
|
@@ -343,10 +361,36 @@ function repositoryFingerprint(
|
|
|
343
361
|
cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
344
362
|
scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
|
|
345
363
|
includeTests: false,
|
|
364
|
+
eventEnvironmentKeys: environmentDeclarations.allowedKeys,
|
|
346
365
|
packageJsonHash: sha256Text(packageJsonText),
|
|
347
366
|
};
|
|
348
367
|
const entries: string[] = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
349
368
|
for (const snapshot of sources.entries())
|
|
350
|
-
entries.push(
|
|
369
|
+
entries.push(
|
|
370
|
+
`${snapshot.filePath}:${
|
|
371
|
+
sourceFactHash(snapshot, environmentDeclarations)}`,
|
|
372
|
+
);
|
|
351
373
|
return sha256Text(entries.join('\n'));
|
|
352
374
|
}
|
|
375
|
+
|
|
376
|
+
function sourceFactHash(
|
|
377
|
+
snapshot: SourceFileSnapshot,
|
|
378
|
+
environmentDeclarations: EnvironmentDeclarationsFact,
|
|
379
|
+
): string {
|
|
380
|
+
if (!isEnvironmentFactInput(snapshot.filePath))
|
|
381
|
+
return sha256Text(snapshot.text);
|
|
382
|
+
const declarations = environmentDeclarations.declarations.filter(
|
|
383
|
+
(item) => item.sourceFile === snapshot.filePath,
|
|
384
|
+
);
|
|
385
|
+
return sha256Text(JSON.stringify({
|
|
386
|
+
allowedKeys: environmentDeclarations.allowedKeys,
|
|
387
|
+
declarations,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isEnvironmentFactInput(filePath: string): boolean {
|
|
392
|
+
const name = filePath.split('/').at(-1);
|
|
393
|
+
return ['nodemon.json', '.env', 'mta.yaml', 'manifest.yml'].includes(
|
|
394
|
+
name ?? '',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
@@ -104,7 +104,12 @@ export interface IndexWorkspaceSummary {
|
|
|
104
104
|
export async function indexWorkspace(
|
|
105
105
|
db: Db,
|
|
106
106
|
workspaceId: number,
|
|
107
|
-
options: {
|
|
107
|
+
options: {
|
|
108
|
+
repo?: string;
|
|
109
|
+
force: boolean;
|
|
110
|
+
eventEnvironmentKeys?: readonly string[];
|
|
111
|
+
injectDerivedMaterializationFailure?: boolean;
|
|
112
|
+
},
|
|
108
113
|
): Promise<IndexWorkspaceSummary> {
|
|
109
114
|
const repos = selectedRepositories(db, workspaceId, options.repo);
|
|
110
115
|
const runId = claimIndexRun(db, workspaceId, repos.length);
|
|
@@ -112,7 +117,9 @@ export async function indexWorkspace(
|
|
|
112
117
|
fileCount: 0, diagnosticCount: 0, skippedCount: 0, rows: [],
|
|
113
118
|
};
|
|
114
119
|
try {
|
|
115
|
-
await prepareRepositories(
|
|
120
|
+
await prepareRepositories(
|
|
121
|
+
repos, options.force, state, options.eventEnvironmentKeys,
|
|
122
|
+
);
|
|
116
123
|
return publishPreparedWorkspaceRows(
|
|
117
124
|
db, workspaceId, runId, state.rows, options,
|
|
118
125
|
);
|
|
@@ -168,10 +175,13 @@ async function prepareRepositories(
|
|
|
168
175
|
repos: readonly IndexRepository[],
|
|
169
176
|
force: boolean,
|
|
170
177
|
state: PreparationState,
|
|
178
|
+
eventEnvironmentKeys?: readonly string[],
|
|
171
179
|
): Promise<void> {
|
|
172
180
|
for (const repo of repos) {
|
|
173
181
|
state.activeRepoId = repo.id;
|
|
174
|
-
const result = await prepareRepositoryIndex(
|
|
182
|
+
const result = await prepareRepositoryIndex(
|
|
183
|
+
repo, force, undefined, eventEnvironmentKeys,
|
|
184
|
+
);
|
|
175
185
|
state.rows.push(result);
|
|
176
186
|
state.fileCount += result.fileCount;
|
|
177
187
|
state.diagnosticCount += result.diagnosticCount;
|
|
@@ -27,6 +27,7 @@ export interface LinkWorkspaceResult {
|
|
|
27
27
|
subscriptionHandlerUnresolvedCount: number;
|
|
28
28
|
subscriptionHandlerMissingAssociationCount: number;
|
|
29
29
|
eventShapeCandidateCount: number;
|
|
30
|
+
eventShapeCandidateOmittedCount: number;
|
|
30
31
|
}
|
|
31
32
|
export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string, string> = {}): LinkWorkspaceResult {
|
|
32
33
|
return db.transaction(() => {
|
|
@@ -46,7 +47,7 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
|
|
|
46
47
|
const impl = linkCanonicalImplementations(db, workspaceId, generation);
|
|
47
48
|
const callSummary = linkCalls(db, workspaceId, vars, generation);
|
|
48
49
|
db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
|
|
49
|
-
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount + eventShapes.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount, eventShapeCandidateCount: eventShapes.edgeCount };
|
|
50
|
+
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount + eventShapes.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount, eventShapeCandidateCount: eventShapes.edgeCount, eventShapeCandidateOmittedCount: eventShapes.omittedCount };
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
function nextGraphGeneration(db: Db, workspaceId: number): number {
|
|
@@ -63,7 +64,8 @@ type CallLinkSummary = Omit<LinkWorkspaceResult,
|
|
|
63
64
|
| 'subscriptionHandlerAmbiguousCount'
|
|
64
65
|
| 'subscriptionHandlerUnresolvedCount'
|
|
65
66
|
| 'subscriptionHandlerMissingAssociationCount'
|
|
66
|
-
| 'eventShapeCandidateCount'
|
|
67
|
+
| 'eventShapeCandidateCount'
|
|
68
|
+
| 'eventShapeCandidateOmittedCount'>;
|
|
67
69
|
|
|
68
70
|
function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, generation: number): CallLinkSummary {
|
|
69
71
|
let edgeCount = 0;
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
2
|
import { parseEventSkeletonFact } from '../utils/event-skeleton.js';
|
|
3
|
+
import { boundCandidateLikeEvidence } from '../utils/bounded-projection.js';
|
|
3
4
|
|
|
4
5
|
export interface EventShapeCandidateLinkSummary {
|
|
5
6
|
edgeCount: number;
|
|
7
|
+
omittedCount: number;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
const EVENT_SHAPE_LINK_CAP_PER_EMIT = 100;
|
|
11
|
+
|
|
8
12
|
interface EventShapeRow extends Record<string, unknown> {
|
|
9
13
|
id: number;
|
|
10
14
|
repoId: number;
|
|
11
15
|
repoName: string;
|
|
12
16
|
signature: string;
|
|
13
17
|
skeletonJson: string;
|
|
18
|
+
evidenceJson: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
interface SubscriberAssociation extends Record<string, unknown> {
|
|
@@ -20,6 +25,7 @@ interface SubscriberAssociation extends Record<string, unknown> {
|
|
|
20
25
|
targetId: string;
|
|
21
26
|
status: string;
|
|
22
27
|
evidenceJson: string;
|
|
28
|
+
targetLabel?: string | null;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
function eventRows(
|
|
@@ -29,7 +35,7 @@ function eventRows(
|
|
|
29
35
|
): EventShapeRow[] {
|
|
30
36
|
return db.prepare(`SELECT c.id,c.repo_id repoId,r.name repoName,
|
|
31
37
|
c.event_skeleton_signature signature,
|
|
32
|
-
c.event_skeleton_json skeletonJson
|
|
38
|
+
c.event_skeleton_json skeletonJson,c.evidence_json evidenceJson
|
|
33
39
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
34
40
|
WHERE r.workspace_id=? AND c.call_type=?
|
|
35
41
|
AND c.event_skeleton_signature IS NOT NULL
|
|
@@ -46,15 +52,19 @@ function associations(
|
|
|
46
52
|
workspaceId: number,
|
|
47
53
|
generation: number,
|
|
48
54
|
): SubscriberAssociation[] {
|
|
49
|
-
return db.prepare(`SELECT id graphEdgeId,
|
|
50
|
-
CAST(json_extract(evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
55
|
+
return db.prepare(`SELECT edge.id graphEdgeId,
|
|
56
|
+
CAST(json_extract(edge.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
51
57
|
subscribeCallId,
|
|
52
|
-
to_kind targetKind,to_id targetId,status,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
edge.to_kind targetKind,edge.to_id targetId,edge.status,
|
|
59
|
+
edge.evidence_json evidenceJson,
|
|
60
|
+
target.source_file || ':' || target.qualified_name targetLabel
|
|
61
|
+
FROM graph_edges edge
|
|
62
|
+
LEFT JOIN symbols target ON edge.to_kind='symbol'
|
|
63
|
+
AND target.id=CAST(edge.to_id AS INTEGER)
|
|
64
|
+
WHERE edge.workspace_id=? AND edge.generation=?
|
|
65
|
+
AND edge.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
66
|
+
AND edge.to_kind='symbol'
|
|
67
|
+
ORDER BY subscribeCallId,edge.id`).all(
|
|
58
68
|
workspaceId, generation,
|
|
59
69
|
) as unknown as SubscriberAssociation[];
|
|
60
70
|
}
|
|
@@ -85,8 +95,11 @@ function candidateEvidence(
|
|
|
85
95
|
emit: EventShapeRow,
|
|
86
96
|
subscribe: EventShapeRow,
|
|
87
97
|
association: SubscriberAssociation,
|
|
98
|
+
total: number,
|
|
99
|
+
shown: number,
|
|
88
100
|
): Record<string, unknown> {
|
|
89
101
|
const evidence = parsedEvidence(association.evidenceJson);
|
|
102
|
+
const parser = parsedEvidence(emit.evidenceJson);
|
|
90
103
|
return {
|
|
91
104
|
publishCallId: emit.id,
|
|
92
105
|
subscribeCallId: subscribe.id,
|
|
@@ -100,7 +113,12 @@ function candidateEvidence(
|
|
|
100
113
|
subscriptionConsumerRepositoryName:
|
|
101
114
|
evidence.subscriptionConsumerRepositoryName,
|
|
102
115
|
handlerSymbolId: Number(association.targetId),
|
|
116
|
+
eventShapeCandidateTargetLabel: association.targetLabel,
|
|
103
117
|
associationGraphEdgeId: association.graphEdgeId,
|
|
118
|
+
outboundEvidence: boundCandidateLikeEvidence(parser),
|
|
119
|
+
eventShapeLinkCandidateCount: total,
|
|
120
|
+
shownEventShapeLinkCandidateCount: shown,
|
|
121
|
+
omittedEventShapeLinkCandidateCount: Math.max(0, total - shown),
|
|
104
122
|
};
|
|
105
123
|
}
|
|
106
124
|
|
|
@@ -111,6 +129,8 @@ function insertCandidate(
|
|
|
111
129
|
emit: EventShapeRow,
|
|
112
130
|
subscribe: EventShapeRow,
|
|
113
131
|
association: SubscriberAssociation,
|
|
132
|
+
total: number,
|
|
133
|
+
shown: number,
|
|
114
134
|
): void {
|
|
115
135
|
db.prepare(`INSERT INTO graph_edges(
|
|
116
136
|
workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
|
|
@@ -125,7 +145,7 @@ function insertCandidate(
|
|
|
125
145
|
association.targetId,
|
|
126
146
|
0.3,
|
|
127
147
|
JSON.stringify(candidateEvidence(
|
|
128
|
-
emit, subscribe, association,
|
|
148
|
+
emit, subscribe, association, total, shown,
|
|
129
149
|
)),
|
|
130
150
|
1,
|
|
131
151
|
'event_skeleton_equivalent_non_authoritative',
|
|
@@ -133,6 +153,24 @@ function insertCandidate(
|
|
|
133
153
|
);
|
|
134
154
|
}
|
|
135
155
|
|
|
156
|
+
interface ShapeCandidate {
|
|
157
|
+
subscription: EventShapeRow;
|
|
158
|
+
association: SubscriberAssociation;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function candidatesForEmit(
|
|
162
|
+
emit: EventShapeRow,
|
|
163
|
+
subscriptions: readonly EventShapeRow[],
|
|
164
|
+
bySubscription: ReadonlyMap<number, SubscriberAssociation[]>,
|
|
165
|
+
): ShapeCandidate[] {
|
|
166
|
+
return subscriptions.flatMap((subscription) =>
|
|
167
|
+
!candidateEligible(emit, subscription) ? []
|
|
168
|
+
: (bySubscription.get(subscription.id) ?? []).map((association) => ({
|
|
169
|
+
subscription,
|
|
170
|
+
association,
|
|
171
|
+
})));
|
|
172
|
+
}
|
|
173
|
+
|
|
136
174
|
export function linkEventShapeCandidates(
|
|
137
175
|
db: Db,
|
|
138
176
|
workspaceId: number,
|
|
@@ -147,15 +185,20 @@ export function linkEventShapeCandidates(
|
|
|
147
185
|
association,
|
|
148
186
|
]);
|
|
149
187
|
let edgeCount = 0;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
188
|
+
let omittedCount = 0;
|
|
189
|
+
for (const emit of emits) {
|
|
190
|
+
const candidates = candidatesForEmit(
|
|
191
|
+
emit, subscriptions, bySubscription,
|
|
192
|
+
);
|
|
193
|
+
const shown = candidates.slice(0, EVENT_SHAPE_LINK_CAP_PER_EMIT);
|
|
194
|
+
for (const candidate of shown) {
|
|
195
|
+
insertCandidate(
|
|
196
|
+
db, workspaceId, generation, emit, candidate.subscription,
|
|
197
|
+
candidate.association, candidates.length, shown.length,
|
|
198
|
+
);
|
|
199
|
+
edgeCount += 1;
|
|
159
200
|
}
|
|
160
|
-
|
|
201
|
+
omittedCount += Math.max(0, candidates.length - shown.length);
|
|
202
|
+
}
|
|
203
|
+
return { edgeCount, omittedCount };
|
|
161
204
|
}
|
|
@@ -33,6 +33,7 @@ interface SubscriptionRow {
|
|
|
33
33
|
packageName?: string | null;
|
|
34
34
|
environmentJson?: string | null;
|
|
35
35
|
eventSkeletonJson?: string | null;
|
|
36
|
+
evidenceJson?: string | null;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
interface HandlerCallRow {
|
|
@@ -73,6 +74,7 @@ function subscriptionRows(db: Db, workspaceId: number): SubscriptionRow[] {
|
|
|
73
74
|
c.call_site_start_offset startOffset,c.call_site_end_offset endOffset,
|
|
74
75
|
c.confidence,c.unresolved_reason unresolvedReason,
|
|
75
76
|
c.event_skeleton_json eventSkeletonJson,
|
|
77
|
+
c.evidence_json evidenceJson,
|
|
76
78
|
r.package_name packageName,
|
|
77
79
|
r.environment_declarations_json environmentJson
|
|
78
80
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
@@ -220,6 +222,7 @@ function evidenceFor(
|
|
|
220
222
|
association: HandlerAssociation,
|
|
221
223
|
event: LinkedEventTemplate,
|
|
222
224
|
environment?: SubscriptionEnvironmentTarget,
|
|
225
|
+
loopValue?: string,
|
|
223
226
|
): Record<string, unknown> {
|
|
224
227
|
const call: Partial<HandlerCallRow> = association.call ?? {};
|
|
225
228
|
const symbolCallReason = boundedSymbolCallReason(call.unresolvedReason);
|
|
@@ -228,8 +231,25 @@ function evidenceFor(
|
|
|
228
231
|
const resolutionStatus = event.isDynamic
|
|
229
232
|
? 'unresolved'
|
|
230
233
|
: environmentAmbiguous ? 'ambiguous' : association.status;
|
|
234
|
+
return {
|
|
235
|
+
...eventDispatchEvidence(subscription, event, environment, loopValue),
|
|
236
|
+
...handlerAssociationEvidence(
|
|
237
|
+
subscription, association, call, resolutionStatus,
|
|
238
|
+
environmentAmbiguous, environment,
|
|
239
|
+
),
|
|
240
|
+
...symbolCallReason,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function eventDispatchEvidence(
|
|
245
|
+
subscription: SubscriptionRow,
|
|
246
|
+
event: LinkedEventTemplate,
|
|
247
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
248
|
+
loopValue?: string,
|
|
249
|
+
): Record<string, unknown> {
|
|
231
250
|
return {
|
|
232
251
|
eventName: subscription.eventName,
|
|
252
|
+
materializedLoopEventName: loopValue,
|
|
233
253
|
...(eventSkeletonEvidence(subscription.eventSkeletonJson)),
|
|
234
254
|
...(event.substitution.placeholders.length > 0 ? {
|
|
235
255
|
effectiveEventName: event.targetId,
|
|
@@ -238,10 +258,6 @@ function evidenceFor(
|
|
|
238
258
|
associationBasis: 'exact_subscription_call_span',
|
|
239
259
|
dispatchScope: 'workspace_event_name_only',
|
|
240
260
|
subscribeCallId: subscription.id,
|
|
241
|
-
symbolCallId: call.id,
|
|
242
|
-
roleSiteMatchCount: association.matchCount,
|
|
243
|
-
callRole: association.matchCount > 0 ? 'event_subscribe_handler' : undefined,
|
|
244
|
-
factOrigin: association.factOrigin ?? call.factOrigin,
|
|
245
261
|
repositoryId: subscription.repoId,
|
|
246
262
|
repositoryName: subscription.repoName,
|
|
247
263
|
subscriptionConsumerRepositoryId: environment?.consumerRepoId,
|
|
@@ -256,6 +272,23 @@ function evidenceFor(
|
|
|
256
272
|
collisionCount: environment?.collisionCount,
|
|
257
273
|
},
|
|
258
274
|
}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function handlerAssociationEvidence(
|
|
279
|
+
subscription: SubscriptionRow,
|
|
280
|
+
association: HandlerAssociation,
|
|
281
|
+
call: Partial<HandlerCallRow>,
|
|
282
|
+
resolutionStatus: string,
|
|
283
|
+
environmentAmbiguous: boolean,
|
|
284
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
285
|
+
): Record<string, unknown> {
|
|
286
|
+
return {
|
|
287
|
+
symbolCallId: call.id,
|
|
288
|
+
roleSiteMatchCount: association.matchCount,
|
|
289
|
+
callRole: association.matchCount > 0
|
|
290
|
+
? 'event_subscribe_handler' : undefined,
|
|
291
|
+
factOrigin: association.factOrigin ?? call.factOrigin,
|
|
259
292
|
sourceFile: subscription.sourceFile,
|
|
260
293
|
sourceLine: subscription.sourceLine,
|
|
261
294
|
callSiteStartOffset: subscription.startOffset,
|
|
@@ -273,7 +306,6 @@ function evidenceFor(
|
|
|
273
306
|
reasonCode: environmentAmbiguous
|
|
274
307
|
? environment?.resolution.reason ?? 'event_environment_value_collision'
|
|
275
308
|
: association.reasonCode,
|
|
276
|
-
...symbolCallReason,
|
|
277
309
|
};
|
|
278
310
|
}
|
|
279
311
|
|
|
@@ -304,20 +336,13 @@ function insertAssociationEdge(
|
|
|
304
336
|
association: HandlerAssociation,
|
|
305
337
|
event: LinkedEventTemplate,
|
|
306
338
|
environment?: SubscriptionEnvironmentTarget,
|
|
339
|
+
loopValue?: string,
|
|
307
340
|
): void {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const reason = event.isDynamic
|
|
314
|
-
? event.substitution.missing.length > 0
|
|
315
|
-
? 'event_template_variables_missing'
|
|
316
|
-
: event.unresolvedReason ?? 'event_name_unsupported_constant_expression'
|
|
317
|
-
: environmentAmbiguous
|
|
318
|
-
? environment?.resolution.reason
|
|
319
|
-
?? 'event_environment_value_collision'
|
|
320
|
-
: association.reasonCode ?? association.call?.unresolvedReason ?? null;
|
|
341
|
+
const ambiguous = environmentTargetAmbiguous(environment);
|
|
342
|
+
const status = associationEdgeStatus(event, association, ambiguous);
|
|
343
|
+
const reason = associationEdgeReason(
|
|
344
|
+
event, association, environment, ambiguous,
|
|
345
|
+
);
|
|
321
346
|
db.prepare(`INSERT INTO graph_edges(
|
|
322
347
|
workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
|
|
323
348
|
confidence,evidence_json,is_dynamic,unresolved_reason,generation
|
|
@@ -331,7 +356,7 @@ function insertAssociationEdge(
|
|
|
331
356
|
association.toId,
|
|
332
357
|
association.call?.confidence ?? subscription.confidence,
|
|
333
358
|
JSON.stringify(evidenceFor(
|
|
334
|
-
subscription, association, event, environment,
|
|
359
|
+
subscription, association, event, environment, loopValue,
|
|
335
360
|
)),
|
|
336
361
|
event.isDynamic ? 1 : 0,
|
|
337
362
|
reason,
|
|
@@ -339,6 +364,66 @@ function insertAssociationEdge(
|
|
|
339
364
|
);
|
|
340
365
|
}
|
|
341
366
|
|
|
367
|
+
function environmentTargetAmbiguous(
|
|
368
|
+
environment?: SubscriptionEnvironmentTarget,
|
|
369
|
+
): boolean {
|
|
370
|
+
return environment?.resolution.status === 'ambiguous'
|
|
371
|
+
|| Number(environment?.collisionCount ?? 1) > 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function associationEdgeStatus(
|
|
375
|
+
event: LinkedEventTemplate,
|
|
376
|
+
association: HandlerAssociation,
|
|
377
|
+
environmentAmbiguous: boolean,
|
|
378
|
+
): 'resolved' | 'ambiguous' | 'unresolved' {
|
|
379
|
+
if (event.isDynamic) return 'unresolved';
|
|
380
|
+
return environmentAmbiguous ? 'ambiguous' : association.status;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function associationEdgeReason(
|
|
384
|
+
event: LinkedEventTemplate,
|
|
385
|
+
association: HandlerAssociation,
|
|
386
|
+
environment: SubscriptionEnvironmentTarget | undefined,
|
|
387
|
+
environmentAmbiguous: boolean,
|
|
388
|
+
): string | null {
|
|
389
|
+
if (event.isDynamic)
|
|
390
|
+
return event.substitution.missing.length > 0
|
|
391
|
+
? 'event_template_variables_missing'
|
|
392
|
+
: event.unresolvedReason ?? 'event_name_unsupported_constant_expression';
|
|
393
|
+
if (environmentAmbiguous)
|
|
394
|
+
return environment?.resolution.reason
|
|
395
|
+
?? 'event_environment_value_collision';
|
|
396
|
+
return association.reasonCode ?? association.call?.unresolvedReason ?? null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parsedEvidenceRecord(
|
|
400
|
+
value: string,
|
|
401
|
+
): Record<string, unknown> | undefined {
|
|
402
|
+
try {
|
|
403
|
+
const parsed: unknown = JSON.parse(value);
|
|
404
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
405
|
+
? parsed as Record<string, unknown> : undefined;
|
|
406
|
+
} catch {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function enumeratedLoopValues(
|
|
412
|
+
evidenceJson: string | null | undefined,
|
|
413
|
+
): string[] | undefined {
|
|
414
|
+
if (!evidenceJson) return undefined;
|
|
415
|
+
const evidence = parsedEvidenceRecord(evidenceJson);
|
|
416
|
+
if (!evidence
|
|
417
|
+
|| evidence.subscriptionLoopRegistrationStatus !== 'enumerated')
|
|
418
|
+
return undefined;
|
|
419
|
+
const values = evidence.subscriptionLoopValues;
|
|
420
|
+
if (!Array.isArray(values)
|
|
421
|
+
|| !values.every((item): item is string => typeof item === 'string'))
|
|
422
|
+
return undefined;
|
|
423
|
+
return evidence.subscriptionLoopRegistrationCount === values.length
|
|
424
|
+
&& evidence.omittedSubscriptionLoopValueCount === 0 ? values : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
342
427
|
function linkedSubscriptionEvents(
|
|
343
428
|
db: Db,
|
|
344
429
|
workspaceId: number,
|
|
@@ -347,24 +432,30 @@ function linkedSubscriptionEvents(
|
|
|
347
432
|
): Array<{
|
|
348
433
|
event: LinkedEventTemplate;
|
|
349
434
|
environment: SubscriptionEnvironmentTarget;
|
|
435
|
+
loopValue?: string;
|
|
350
436
|
}> {
|
|
351
437
|
const skeleton = parseEventSkeletonFact(subscription.eventSkeletonJson);
|
|
352
|
-
|
|
438
|
+
const environments = subscriptionEnvironmentTargets(
|
|
353
439
|
db,
|
|
354
440
|
workspaceId,
|
|
355
441
|
subscription.packageName ?? undefined,
|
|
356
442
|
subscription.eventSkeletonJson,
|
|
357
443
|
subscription.environmentJson,
|
|
358
444
|
variables,
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
445
|
+
);
|
|
446
|
+
const loopValues = enumeratedLoopValues(subscription.evidenceJson);
|
|
447
|
+
const templates = loopValues ?? [subscription.eventName];
|
|
448
|
+
return environments.flatMap((environment) =>
|
|
449
|
+
templates.map((template) => ({
|
|
450
|
+
environment,
|
|
451
|
+
loopValue: loopValues ? template : undefined,
|
|
452
|
+
event: linkEventTemplate(
|
|
453
|
+
template,
|
|
454
|
+
environment.resolution.variables,
|
|
455
|
+
loopValues ? undefined : subscription.unresolvedReason ?? undefined,
|
|
456
|
+
loopValues ? undefined : skeleton,
|
|
457
|
+
),
|
|
458
|
+
})));
|
|
368
459
|
}
|
|
369
460
|
|
|
370
461
|
function targetStatus(
|
|
@@ -414,7 +505,7 @@ export function linkEventSubscriptionHandlers(
|
|
|
414
505
|
)) {
|
|
415
506
|
insertAssociationEdge(
|
|
416
507
|
db, workspaceId, generation, subscription, association,
|
|
417
|
-
target.event, target.environment,
|
|
508
|
+
target.event, target.environment, target.loopValue,
|
|
418
509
|
);
|
|
419
510
|
incrementSummary(
|
|
420
511
|
summary,
|