@saptools/service-flow 0.1.70 → 0.1.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +23 -6
- package/TECHNICAL-NOTE.md +24 -2
- package/dist/{chunk-GSLFY6J2.js → chunk-32WOTGTS.js} +12061 -9288
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +854 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -14
- package/dist/index.js +2 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +446 -0
- package/src/cli/doctor.ts +4 -6
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -29
- package/src/db/event-fact-semantics.ts +546 -0
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +15 -1
- package/src/db/package-target-invalidation.ts +80 -0
- package/src/db/repositories.ts +28 -2
- package/src/db/schema-structure.ts +41 -1
- package/src/db/schema.ts +6 -2
- package/src/indexer/repository-indexer.ts +93 -10
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +27 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +204 -0
- package/src/linker/event-subscription-handler-linker.ts +220 -25
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +302 -0
- package/src/output/repository-inspection.ts +11 -0
- package/src/output/table-output.ts +13 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +404 -0
- package/src/parsers/event-call-analysis.ts +370 -0
- package/src/parsers/event-environment-reference.ts +242 -0
- package/src/parsers/event-loop-registration.ts +132 -0
- package/src/parsers/event-name-import-resolution.ts +243 -0
- package/src/parsers/event-receiver-analysis.ts +394 -0
- package/src/parsers/event-subscription-facts.ts +4 -0
- package/src/parsers/generated-constants-parser.ts +80 -14
- package/src/parsers/outbound-call-classifier.ts +27 -124
- package/src/parsers/outbound-call-parser.ts +13 -1
- package/src/parsers/outbound-expression-analysis.ts +2 -2
- package/src/parsers/stable-local-value.ts +42 -10
- package/src/parsers/string-constant-lookups.ts +408 -0
- package/src/trace/edge-target.ts +65 -0
- package/src/trace/event-runtime-resolution.ts +24 -3
- package/src/trace/event-shape-candidate-trace.ts +172 -0
- package/src/trace/event-subscriber-traversal.ts +90 -8
- package/src/trace/evidence.ts +7 -28
- package/src/trace/trace-scope-execution.ts +22 -30
- package/src/types.ts +19 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
3
|
+
import {
|
|
4
|
+
parseEnvironmentDeclarationsFact,
|
|
5
|
+
} from '../parsers/environment-declarations.js';
|
|
6
|
+
import type {
|
|
7
|
+
EventEnvironmentReference,
|
|
8
|
+
} from '../parsers/event-environment-reference.js';
|
|
9
|
+
import { parseEventSkeletonFact } from '../utils/event-skeleton.js';
|
|
10
|
+
import { parsePackageImportReference } from
|
|
11
|
+
'../parsers/package-fact-contract.js';
|
|
12
|
+
import {
|
|
13
|
+
expectedPackageEventConstantResolution,
|
|
14
|
+
type PackageEventConstantResolution,
|
|
15
|
+
} from '../linker/package-event-constant-resolver.js';
|
|
16
|
+
|
|
17
|
+
export interface EventFactSemanticCategoryCount {
|
|
18
|
+
category: string;
|
|
19
|
+
count: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EventFactRow extends Record<string, unknown> {
|
|
23
|
+
id?: number;
|
|
24
|
+
repoId?: number;
|
|
25
|
+
repositoryName?: string;
|
|
26
|
+
workspaceId?: number;
|
|
27
|
+
sourceFile?: string;
|
|
28
|
+
sourceLine?: number;
|
|
29
|
+
eventName?: string;
|
|
30
|
+
skeletonSignature?: string | null;
|
|
31
|
+
skeletonJson?: string | null;
|
|
32
|
+
unresolvedReason?: string | null;
|
|
33
|
+
evidenceJson?: string;
|
|
34
|
+
environmentJson?: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface InvalidEventFactExample {
|
|
38
|
+
category: string;
|
|
39
|
+
repositoryId: number;
|
|
40
|
+
repositoryName: string;
|
|
41
|
+
sourceFile?: string;
|
|
42
|
+
sourceLine?: number;
|
|
43
|
+
factId?: number;
|
|
44
|
+
failingPredicate: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const receiverReasons = new Set([
|
|
48
|
+
'event_receiver_unproven_binding',
|
|
49
|
+
'event_receiver_unproven_propagation',
|
|
50
|
+
'event_receiver_not_cap_client',
|
|
51
|
+
]);
|
|
52
|
+
const constantReasons = new Set([
|
|
53
|
+
'event_name_constant_container_ambiguous',
|
|
54
|
+
'event_name_constant_member_not_string',
|
|
55
|
+
'event_name_constant_container_mutable',
|
|
56
|
+
'event_name_constant_container_unsafe_reference',
|
|
57
|
+
'event_name_constant_container_unsupported_shape',
|
|
58
|
+
'event_name_constant_container_not_exported',
|
|
59
|
+
'event_name_constant_resolution_pending',
|
|
60
|
+
'event_name_constant_value_empty',
|
|
61
|
+
]);
|
|
62
|
+
function category(
|
|
63
|
+
name: string,
|
|
64
|
+
count: number,
|
|
65
|
+
): EventFactSemanticCategoryCount[] {
|
|
66
|
+
return count > 0 ? [{ category: name, count }] : [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
70
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
71
|
+
? value as Record<string, unknown> : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function jsonRecord(value: unknown): Record<string, unknown> | undefined {
|
|
75
|
+
if (typeof value !== 'string') return undefined;
|
|
76
|
+
try {
|
|
77
|
+
return record(JSON.parse(value) as unknown);
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function currentEventRows(
|
|
84
|
+
db: Db,
|
|
85
|
+
workspaceId?: number,
|
|
86
|
+
): EventFactRow[] {
|
|
87
|
+
return db.prepare(`SELECT fact.id,fact.repo_id repoId,r.name repositoryName,
|
|
88
|
+
fact.source_file sourceFile,fact.source_line sourceLine,
|
|
89
|
+
fact.event_name_expr eventName,
|
|
90
|
+
r.workspace_id workspaceId,
|
|
91
|
+
fact.event_skeleton_signature skeletonSignature,
|
|
92
|
+
fact.event_skeleton_json skeletonJson,
|
|
93
|
+
fact.unresolved_reason unresolvedReason,
|
|
94
|
+
fact.evidence_json evidenceJson,
|
|
95
|
+
r.environment_declarations_json environmentJson
|
|
96
|
+
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
97
|
+
WHERE r.fact_analyzer_version=?
|
|
98
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
99
|
+
AND fact.call_type IN ('async_emit','async_subscribe')`).all(
|
|
100
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
101
|
+
) as EventFactRow[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function eventInvalidPredicate(
|
|
105
|
+
db: Db,
|
|
106
|
+
row: EventFactRow,
|
|
107
|
+
phase: 'pre_package' | 'terminal',
|
|
108
|
+
): string | undefined {
|
|
109
|
+
if (typeof row.eventName !== 'string' || row.eventName.length === 0)
|
|
110
|
+
return 'event_name_non_empty';
|
|
111
|
+
const evidence = jsonRecord(row.evidenceJson);
|
|
112
|
+
if (!evidence) return 'event_evidence_valid_json_object';
|
|
113
|
+
if (!receiverEvidenceValid(evidence))
|
|
114
|
+
return 'event_receiver_evidence_consistent';
|
|
115
|
+
if (!eventReasonValid(row, evidence))
|
|
116
|
+
return 'event_name_reason_axis_consistent';
|
|
117
|
+
if (!packageConstantValid(db, row, evidence, phase))
|
|
118
|
+
return 'event_package_constant_resolution_consistent';
|
|
119
|
+
return skeletonValid(row, evidence)
|
|
120
|
+
? undefined : 'event_skeleton_complete_and_signed';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function integerField(
|
|
124
|
+
value: Record<string, unknown>,
|
|
125
|
+
key: string,
|
|
126
|
+
): number | undefined {
|
|
127
|
+
const item = value[key];
|
|
128
|
+
return Number.isInteger(item) && Number(item) >= 0
|
|
129
|
+
? Number(item) : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function packagePendingValid(
|
|
133
|
+
row: EventFactRow,
|
|
134
|
+
evidence: Record<string, unknown>,
|
|
135
|
+
): boolean {
|
|
136
|
+
const source = evidence.eventNameConstantSourceExpression;
|
|
137
|
+
return evidence.eventNameUnresolvedReason
|
|
138
|
+
=== 'event_name_constant_resolution_pending'
|
|
139
|
+
&& evidence.eventNamePackageConstantResolution === undefined
|
|
140
|
+
&& typeof source === 'string' && source.length > 0
|
|
141
|
+
&& row.eventName === source;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolutionFieldsValid(
|
|
145
|
+
value: Record<string, unknown>,
|
|
146
|
+
expected: PackageEventConstantResolution,
|
|
147
|
+
): boolean {
|
|
148
|
+
return value.status === expected.status
|
|
149
|
+
&& value.reason === expected.reason
|
|
150
|
+
&& integerField(value, 'candidateCount') === expected.candidateCount
|
|
151
|
+
&& integerField(value, 'eligibleCandidateCount')
|
|
152
|
+
=== expected.eligibleCandidateCount
|
|
153
|
+
&& integerField(value, 'selectedCandidateCount')
|
|
154
|
+
=== (expected.status === 'resolved' ? 1 : 0)
|
|
155
|
+
&& value.candidateSetComplete === expected.complete
|
|
156
|
+
&& value.resolvedModulePath === expected.modulePath
|
|
157
|
+
&& value.targetRepositoryId === expected.targetRepoId;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function packageTerminalValid(
|
|
161
|
+
db: Db,
|
|
162
|
+
row: EventFactRow,
|
|
163
|
+
evidence: Record<string, unknown>,
|
|
164
|
+
): boolean {
|
|
165
|
+
const binding = parsePackageImportReference(
|
|
166
|
+
evidence.eventNameConstantImportBinding,
|
|
167
|
+
);
|
|
168
|
+
const workspaceId = row.workspaceId;
|
|
169
|
+
const resolution = record(evidence.eventNamePackageConstantResolution);
|
|
170
|
+
if (!binding || typeof workspaceId !== 'number' || !resolution)
|
|
171
|
+
return false;
|
|
172
|
+
const expected = expectedPackageEventConstantResolution(
|
|
173
|
+
db, workspaceId, binding,
|
|
174
|
+
);
|
|
175
|
+
const source = evidence.eventNameConstantSourceExpression;
|
|
176
|
+
const expectedName = expected.value ?? source;
|
|
177
|
+
const expectedReason = expected.status === 'resolved'
|
|
178
|
+
? undefined : expected.reason;
|
|
179
|
+
if (row.eventName !== expectedName
|
|
180
|
+
|| evidence.eventNameUnresolvedReason !== expectedReason
|
|
181
|
+
|| !resolutionFieldsValid(resolution, expected)) return false;
|
|
182
|
+
if (expected.status !== 'resolved') return true;
|
|
183
|
+
const constant = record(evidence.eventNameConstant);
|
|
184
|
+
return constant?.sourceKind === 'package_static_string'
|
|
185
|
+
&& constant.sourceFile === expected.sourceFile
|
|
186
|
+
&& constant.sourceLine === expected.sourceLine;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function packageConstantValid(
|
|
190
|
+
db: Db,
|
|
191
|
+
row: EventFactRow,
|
|
192
|
+
evidence: Record<string, unknown>,
|
|
193
|
+
phase: 'pre_package' | 'terminal',
|
|
194
|
+
): boolean {
|
|
195
|
+
if (evidence.eventNameConstantImportBinding === undefined) return true;
|
|
196
|
+
const binding = parsePackageImportReference(
|
|
197
|
+
evidence.eventNameConstantImportBinding,
|
|
198
|
+
);
|
|
199
|
+
const source = evidence.eventNameConstantSourceExpression;
|
|
200
|
+
if (!binding || typeof source !== 'string' || source.length === 0)
|
|
201
|
+
return false;
|
|
202
|
+
if (packagePendingValid(row, evidence))
|
|
203
|
+
return phase === 'pre_package';
|
|
204
|
+
return packageTerminalValid(db, row, evidence);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function receiverEvidenceValid(
|
|
208
|
+
evidence: Record<string, unknown>,
|
|
209
|
+
): boolean {
|
|
210
|
+
const classification = evidence.receiverClassification;
|
|
211
|
+
const proof = evidence.receiverProof;
|
|
212
|
+
if (!receiverEvidenceShapeValid(classification, proof, evidence))
|
|
213
|
+
return false;
|
|
214
|
+
if (classification === 'name_fallback')
|
|
215
|
+
return fallbackReceiverEvidenceValid(proof, evidence);
|
|
216
|
+
if (classification !== 'unproven')
|
|
217
|
+
return evidence.receiverUnresolvedReason === undefined
|
|
218
|
+
&& evidence.receiverFallbackRefusedReason === undefined;
|
|
219
|
+
return evidence.receiverFallbackRefusedReason === undefined
|
|
220
|
+
&& typeof evidence.receiverUnresolvedReason === 'string'
|
|
221
|
+
&& receiverReasons.has(evidence.receiverUnresolvedReason);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function receiverEvidenceShapeValid(
|
|
225
|
+
classification: unknown,
|
|
226
|
+
proof: unknown,
|
|
227
|
+
evidence: Record<string, unknown>,
|
|
228
|
+
): boolean {
|
|
229
|
+
const sites = evidence.consideredBindingSites;
|
|
230
|
+
return ['cap_evidence', 'name_fallback', 'unproven'].includes(
|
|
231
|
+
String(classification),
|
|
232
|
+
) && typeof proof === 'string' && proof.length > 0
|
|
233
|
+
&& Array.isArray(sites) && sites.length <= 8;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fallbackReceiverEvidenceValid(
|
|
237
|
+
proof: unknown,
|
|
238
|
+
evidence: Record<string, unknown>,
|
|
239
|
+
): boolean {
|
|
240
|
+
const reason = evidence.receiverFallbackRefusedReason;
|
|
241
|
+
return proof === 'compatibility_name_fallback'
|
|
242
|
+
&& evidence.receiverUnresolvedReason === undefined
|
|
243
|
+
&& typeof reason === 'string' && reason.length > 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function environmentBindingValid(
|
|
247
|
+
binding: EventEnvironmentReference,
|
|
248
|
+
sourceKeys: readonly string[],
|
|
249
|
+
allowedKeys: ReadonlySet<string>,
|
|
250
|
+
): boolean {
|
|
251
|
+
if (!['resolved', 'refused'].includes(binding.status)
|
|
252
|
+
|| !sourceKeys.includes(binding.sourceKey)
|
|
253
|
+
|| !environmentTransformsValid(binding.transforms)) return false;
|
|
254
|
+
if (binding.status === 'refused')
|
|
255
|
+
return typeof binding.reason === 'string' && binding.reason.length > 0;
|
|
256
|
+
return resolvedEnvironmentBindingValid(binding, allowedKeys);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function environmentTransformsValid(value: unknown): boolean {
|
|
260
|
+
return Array.isArray(value) && value.every((item) =>
|
|
261
|
+
item === 'toUpperCase' || item === 'toLowerCase');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolvedEnvironmentBindingValid(
|
|
265
|
+
binding: EventEnvironmentReference,
|
|
266
|
+
allowedKeys: ReadonlySet<string>,
|
|
267
|
+
): boolean {
|
|
268
|
+
const key = binding.environmentKey;
|
|
269
|
+
const file = binding.sourceFile;
|
|
270
|
+
return typeof key === 'string' && allowedKeys.has(key)
|
|
271
|
+
&& typeof file === 'string' && file.length > 0
|
|
272
|
+
&& Number.isInteger(binding.startOffset)
|
|
273
|
+
&& Number(binding.startOffset) >= 0
|
|
274
|
+
&& Number.isInteger(binding.endOffset)
|
|
275
|
+
&& Number(binding.endOffset) > Number(binding.startOffset);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function skeletonValid(
|
|
279
|
+
row: EventFactRow,
|
|
280
|
+
evidence: Record<string, unknown>,
|
|
281
|
+
): boolean {
|
|
282
|
+
const reason = evidence.eventNameUnresolvedReason;
|
|
283
|
+
if (reason !== 'dynamic_event_name_identifier')
|
|
284
|
+
return row.skeletonSignature == null && row.skeletonJson == null;
|
|
285
|
+
const skeleton = parseEventSkeletonFact(row.skeletonJson);
|
|
286
|
+
const environment = parseEnvironmentDeclarationsFact(row.environmentJson);
|
|
287
|
+
if (!skeleton || skeleton.status !== 'complete'
|
|
288
|
+
|| !environment
|
|
289
|
+
|| row.skeletonSignature !== skeleton.signature
|
|
290
|
+
|| !skeleton.environmentBindings.every((binding) =>
|
|
291
|
+
environmentBindingValid(
|
|
292
|
+
binding, skeleton.sourceKeys, new Set(environment.allowedKeys),
|
|
293
|
+
))) return false;
|
|
294
|
+
const keys = evidence.eventNamePlaceholderKeys;
|
|
295
|
+
return Array.isArray(keys)
|
|
296
|
+
&& JSON.stringify(keys) === JSON.stringify(skeleton.sourceKeys);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function eventReasonValid(
|
|
300
|
+
row: EventFactRow,
|
|
301
|
+
evidence: Record<string, unknown>,
|
|
302
|
+
): boolean {
|
|
303
|
+
const eventReason = evidence.eventNameUnresolvedReason;
|
|
304
|
+
if (eventReason === undefined) return row.unresolvedReason == null;
|
|
305
|
+
if (eventReason === 'dynamic_event_name_identifier'
|
|
306
|
+
|| eventReason === 'dynamic_event_name_unsupported_expression'
|
|
307
|
+
|| constantReasons.has(String(eventReason)))
|
|
308
|
+
return row.unresolvedReason === eventReason;
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function invalidEventRows(
|
|
313
|
+
db: Db,
|
|
314
|
+
workspaceId?: number,
|
|
315
|
+
phase: 'pre_package' | 'terminal' = 'pre_package',
|
|
316
|
+
): number {
|
|
317
|
+
return currentEventRows(db, workspaceId).filter((row) =>
|
|
318
|
+
eventInvalidPredicate(db, row, phase) !== undefined).length;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function invalidEnvironmentRepositories(
|
|
322
|
+
db: Db,
|
|
323
|
+
workspaceId?: number,
|
|
324
|
+
): number {
|
|
325
|
+
const rows = db.prepare(`SELECT environment_declarations_json value
|
|
326
|
+
FROM repositories WHERE fact_analyzer_version=?
|
|
327
|
+
AND (? IS NULL OR workspace_id=?)`).all(
|
|
328
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
329
|
+
);
|
|
330
|
+
return rows.filter((row) =>
|
|
331
|
+
!parseEnvironmentDeclarationsFact(row.value)).length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function generatedConstantInvalidPredicate(
|
|
335
|
+
row: Record<string, unknown>,
|
|
336
|
+
): string | undefined {
|
|
337
|
+
const kind = String(row.constantKind);
|
|
338
|
+
if (!['const_identifier', 'enum_member', 'const_object_property']
|
|
339
|
+
.includes(kind)) return 'generated_constant_kind_known';
|
|
340
|
+
if (typeof row.name !== 'string' || row.name.length === 0)
|
|
341
|
+
return 'generated_constant_name_non_empty';
|
|
342
|
+
const status = generatedConstantStatusInvalid(row);
|
|
343
|
+
if (status) return status;
|
|
344
|
+
const source = generatedConstantSourceInvalid(row);
|
|
345
|
+
if (source) return source;
|
|
346
|
+
if (!validConstantOffsets(row)) return 'generated_constant_offsets_valid';
|
|
347
|
+
return generatedConstantMemberShapeValid(row, kind)
|
|
348
|
+
? undefined : 'generated_constant_member_shape_valid';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function generatedConstantStatusInvalid(
|
|
352
|
+
row: Record<string, unknown>,
|
|
353
|
+
): string | undefined {
|
|
354
|
+
const resolved = row.resolutionStatus === 'resolved';
|
|
355
|
+
const refused = row.resolutionStatus === 'refused';
|
|
356
|
+
if (!resolved && !refused) return 'generated_constant_status_known';
|
|
357
|
+
if (resolved && (typeof row.value !== 'string'
|
|
358
|
+
|| row.unresolvedReason != null))
|
|
359
|
+
return 'generated_constant_resolved_value_consistent';
|
|
360
|
+
return refused && (row.value != null
|
|
361
|
+
|| !constantReasons.has(String(row.unresolvedReason)))
|
|
362
|
+
? 'generated_constant_refusal_consistent' : undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function generatedConstantSourceInvalid(
|
|
366
|
+
row: Record<string, unknown>,
|
|
367
|
+
): string | undefined {
|
|
368
|
+
if (typeof row.sourceFile !== 'string' || row.sourceFile.length === 0
|
|
369
|
+
|| !Number.isInteger(row.sourceLine) || Number(row.sourceLine) < 1)
|
|
370
|
+
return 'generated_constant_source_location_valid';
|
|
371
|
+
return [0, 1].includes(Number(row.exported))
|
|
372
|
+
&& [0, 1].includes(Number(row.stable))
|
|
373
|
+
? undefined : 'generated_constant_flags_boolean';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function generatedConstantMemberShapeValid(
|
|
377
|
+
row: Record<string, unknown>,
|
|
378
|
+
kind: string,
|
|
379
|
+
): boolean {
|
|
380
|
+
if (kind === 'const_identifier')
|
|
381
|
+
return row.containerName == null && row.memberName == null;
|
|
382
|
+
const container = row.containerName;
|
|
383
|
+
const member = row.memberName;
|
|
384
|
+
return typeof container === 'string' && container.length > 0
|
|
385
|
+
&& typeof member === 'string' && member.length > 0
|
|
386
|
+
&& row.name === `${container}.${member}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function validConstantOffsets(row: Record<string, unknown>): boolean {
|
|
390
|
+
const start = row.declarationStartOffset;
|
|
391
|
+
const end = row.declarationEndOffset;
|
|
392
|
+
const valueStart = row.valueStartOffset;
|
|
393
|
+
const valueEnd = row.valueEndOffset;
|
|
394
|
+
return Number.isInteger(start) && Number(start) >= 0
|
|
395
|
+
&& Number.isInteger(end) && Number(end) > Number(start)
|
|
396
|
+
&& Number.isInteger(valueStart) && Number(valueStart) >= Number(start)
|
|
397
|
+
&& Number.isInteger(valueEnd) && Number(valueEnd) > Number(valueStart)
|
|
398
|
+
&& Number(valueEnd) <= Number(end);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function invalidGeneratedConstants(
|
|
402
|
+
db: Db,
|
|
403
|
+
workspaceId?: number,
|
|
404
|
+
): number {
|
|
405
|
+
const rows = generatedConstantRows(db, workspaceId);
|
|
406
|
+
return rows.filter((row) =>
|
|
407
|
+
generatedConstantInvalidPredicate(row) !== undefined).length;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function generatedConstantRows(
|
|
411
|
+
db: Db,
|
|
412
|
+
workspaceId?: number,
|
|
413
|
+
): Array<Record<string, unknown>> {
|
|
414
|
+
return db.prepare(`SELECT fact.id,fact.repo_id repoId,
|
|
415
|
+
r.name repositoryName,fact.source_file sourceFile,
|
|
416
|
+
fact.source_line sourceLine,fact.name,fact.container_name containerName,
|
|
417
|
+
fact.member_name memberName,fact.value,
|
|
418
|
+
fact.constant_kind constantKind,fact.exported,fact.stable,
|
|
419
|
+
fact.resolution_status resolutionStatus,
|
|
420
|
+
fact.unresolved_reason unresolvedReason,
|
|
421
|
+
fact.declaration_start_offset declarationStartOffset,
|
|
422
|
+
fact.declaration_end_offset declarationEndOffset,
|
|
423
|
+
fact.value_start_offset valueStartOffset,
|
|
424
|
+
fact.value_end_offset valueEndOffset
|
|
425
|
+
FROM generated_constants fact
|
|
426
|
+
JOIN repositories r ON r.id=fact.repo_id
|
|
427
|
+
WHERE r.fact_analyzer_version=?
|
|
428
|
+
AND (? IS NULL OR r.workspace_id=?)`).all(
|
|
429
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function eventFactExamples(
|
|
434
|
+
db: Db,
|
|
435
|
+
workspaceId: number | undefined,
|
|
436
|
+
phase: 'pre_package' | 'terminal',
|
|
437
|
+
): InvalidEventFactExample[] {
|
|
438
|
+
return currentEventRows(db, workspaceId).flatMap((row) => {
|
|
439
|
+
const predicate = eventInvalidPredicate(db, row, phase);
|
|
440
|
+
return predicate && typeof row.repoId === 'number'
|
|
441
|
+
&& typeof row.repositoryName === 'string'
|
|
442
|
+
? [{
|
|
443
|
+
category: 'event_fact_semantics_invalid',
|
|
444
|
+
repositoryId: row.repoId,
|
|
445
|
+
repositoryName: row.repositoryName,
|
|
446
|
+
sourceFile: row.sourceFile,
|
|
447
|
+
sourceLine: row.sourceLine,
|
|
448
|
+
factId: row.id,
|
|
449
|
+
failingPredicate: predicate,
|
|
450
|
+
}] : [];
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function generatedConstantExamples(
|
|
455
|
+
db: Db,
|
|
456
|
+
workspaceId?: number,
|
|
457
|
+
): InvalidEventFactExample[] {
|
|
458
|
+
return generatedConstantRows(db, workspaceId).flatMap((row) => {
|
|
459
|
+
const predicate = generatedConstantInvalidPredicate(row);
|
|
460
|
+
return predicate && typeof row.repoId === 'number'
|
|
461
|
+
&& typeof row.repositoryName === 'string'
|
|
462
|
+
? [{
|
|
463
|
+
category: 'generated_constant_fact_invalid',
|
|
464
|
+
repositoryId: row.repoId,
|
|
465
|
+
repositoryName: row.repositoryName,
|
|
466
|
+
sourceFile: typeof row.sourceFile === 'string'
|
|
467
|
+
? row.sourceFile : undefined,
|
|
468
|
+
sourceLine: typeof row.sourceLine === 'number'
|
|
469
|
+
? row.sourceLine : undefined,
|
|
470
|
+
factId: typeof row.id === 'number' ? row.id : undefined,
|
|
471
|
+
failingPredicate: predicate,
|
|
472
|
+
}] : [];
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function environmentExamples(
|
|
477
|
+
db: Db,
|
|
478
|
+
workspaceId?: number,
|
|
479
|
+
): InvalidEventFactExample[] {
|
|
480
|
+
const rows = db.prepare(`SELECT id repositoryId,name repositoryName,
|
|
481
|
+
environment_declarations_json value FROM repositories
|
|
482
|
+
WHERE fact_analyzer_version=?
|
|
483
|
+
AND (? IS NULL OR workspace_id=?)
|
|
484
|
+
ORDER BY name COLLATE BINARY,id`).all(
|
|
485
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
486
|
+
);
|
|
487
|
+
return rows.flatMap((row) =>
|
|
488
|
+
!parseEnvironmentDeclarationsFact(row.value)
|
|
489
|
+
&& typeof row.repositoryId === 'number'
|
|
490
|
+
&& typeof row.repositoryName === 'string'
|
|
491
|
+
? [{
|
|
492
|
+
category: 'repository_environment_declarations_invalid',
|
|
493
|
+
repositoryId: row.repositoryId,
|
|
494
|
+
repositoryName: row.repositoryName,
|
|
495
|
+
failingPredicate: 'environment_declarations_fact_valid',
|
|
496
|
+
}] : []);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function compareExample(
|
|
500
|
+
left: InvalidEventFactExample,
|
|
501
|
+
right: InvalidEventFactExample,
|
|
502
|
+
): number {
|
|
503
|
+
const leftKey = `${left.repositoryName}\0${left.sourceFile ?? ''}\0${
|
|
504
|
+
left.sourceLine ?? 0}\0${left.category}\0${left.factId ?? 0}`;
|
|
505
|
+
const rightKey = `${right.repositoryName}\0${right.sourceFile ?? ''}\0${
|
|
506
|
+
right.sourceLine ?? 0}\0${right.category}\0${right.factId ?? 0}`;
|
|
507
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function invalidEventFactExamples(
|
|
511
|
+
db: Db,
|
|
512
|
+
workspaceId?: number,
|
|
513
|
+
phase: 'pre_package' | 'terminal' = 'pre_package',
|
|
514
|
+
limit = 5,
|
|
515
|
+
): {
|
|
516
|
+
total: number;
|
|
517
|
+
affectedRepositoryCount: number;
|
|
518
|
+
examples: InvalidEventFactExample[];
|
|
519
|
+
} {
|
|
520
|
+
const all = [
|
|
521
|
+
...eventFactExamples(db, workspaceId, phase),
|
|
522
|
+
...generatedConstantExamples(db, workspaceId),
|
|
523
|
+
...environmentExamples(db, workspaceId),
|
|
524
|
+
].sort(compareExample);
|
|
525
|
+
return {
|
|
526
|
+
total: all.length,
|
|
527
|
+
affectedRepositoryCount:
|
|
528
|
+
new Set(all.map((item) => item.repositoryId)).size,
|
|
529
|
+
examples: all.slice(0, Math.max(0, limit)),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function invalidEventFactCategories(
|
|
534
|
+
db: Db,
|
|
535
|
+
workspaceId?: number,
|
|
536
|
+
phase: 'pre_package' | 'terminal' = 'pre_package',
|
|
537
|
+
): EventFactSemanticCategoryCount[] {
|
|
538
|
+
return [
|
|
539
|
+
...category('event_fact_semantics_invalid',
|
|
540
|
+
invalidEventRows(db, workspaceId, phase)),
|
|
541
|
+
...category('repository_environment_declarations_invalid',
|
|
542
|
+
invalidEnvironmentRepositories(db, workspaceId)),
|
|
543
|
+
...category('generated_constant_fact_invalid',
|
|
544
|
+
invalidGeneratedConstants(db, workspaceId)),
|
|
545
|
+
];
|
|
546
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
3
|
+
|
|
4
|
+
export interface EventSiteCategoryCount {
|
|
5
|
+
category: string;
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function count(
|
|
10
|
+
db: Db,
|
|
11
|
+
sql: string,
|
|
12
|
+
workspaceId?: number,
|
|
13
|
+
): number {
|
|
14
|
+
const row = db.prepare(sql).get(
|
|
15
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
16
|
+
);
|
|
17
|
+
return Number(row?.count ?? 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function category(
|
|
21
|
+
name: string,
|
|
22
|
+
value: number,
|
|
23
|
+
): EventSiteCategoryCount[] {
|
|
24
|
+
return value > 0 ? [{ category: name, count: value }] : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function duplicateSiteCount(
|
|
28
|
+
db: Db,
|
|
29
|
+
workspaceId: number | undefined,
|
|
30
|
+
callType: 'async_emit' | 'async_subscribe',
|
|
31
|
+
): number {
|
|
32
|
+
return count(db, `SELECT COUNT(*) count FROM (
|
|
33
|
+
SELECT fact.repo_id,fact.source_file,fact.call_site_start_offset,
|
|
34
|
+
fact.call_site_end_offset,COUNT(*) duplicate_count
|
|
35
|
+
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
36
|
+
WHERE r.fact_analyzer_version=?
|
|
37
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
38
|
+
AND fact.call_type='${callType}'
|
|
39
|
+
GROUP BY fact.repo_id,fact.source_file,fact.call_site_start_offset,
|
|
40
|
+
fact.call_site_end_offset HAVING COUNT(*)<>1
|
|
41
|
+
)`, workspaceId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function eventSiteCategories(
|
|
45
|
+
db: Db,
|
|
46
|
+
workspaceId?: number,
|
|
47
|
+
): EventSiteCategoryCount[] {
|
|
48
|
+
const invalidName = count(db, `SELECT COUNT(*) count
|
|
49
|
+
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
50
|
+
WHERE r.fact_analyzer_version=?
|
|
51
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
52
|
+
AND fact.call_type IN ('async_emit','async_subscribe')
|
|
53
|
+
AND (typeof(fact.event_name_expr)<>'text'
|
|
54
|
+
OR length(fact.event_name_expr)=0)`, workspaceId);
|
|
55
|
+
return [
|
|
56
|
+
...category('event_name_invalid', invalidName),
|
|
57
|
+
...category('async_subscription_site_duplicate',
|
|
58
|
+
duplicateSiteCount(db, workspaceId, 'async_subscribe')),
|
|
59
|
+
...category('async_emit_site_duplicate',
|
|
60
|
+
duplicateSiteCount(db, workspaceId, 'async_emit')),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import type { OutboundCallFact } from '../types.js';
|
|
3
|
+
|
|
4
|
+
function priorEventFactCount(db: Db, repoId: number): number {
|
|
5
|
+
const row = db.prepare(`SELECT COUNT(*) count FROM outbound_calls
|
|
6
|
+
WHERE repo_id=? AND call_type IN ('async_emit','async_subscribe')`).get(
|
|
7
|
+
repoId,
|
|
8
|
+
);
|
|
9
|
+
return Number(row?.count ?? 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function repositoryEnvironment(
|
|
13
|
+
db: Db,
|
|
14
|
+
repoId: number,
|
|
15
|
+
): string | null {
|
|
16
|
+
const row = db.prepare(`SELECT environment_declarations_json value
|
|
17
|
+
FROM repositories WHERE id=?`).get(repoId);
|
|
18
|
+
return typeof row?.value === 'string' ? row.value : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function invalidateEventSurfaceFacts(
|
|
22
|
+
db: Db,
|
|
23
|
+
repoId: number,
|
|
24
|
+
calls: readonly OutboundCallFact[],
|
|
25
|
+
nextEnvironmentJson: string,
|
|
26
|
+
): void {
|
|
27
|
+
const hasNewEvents = calls.some((call) =>
|
|
28
|
+
call.callType === 'async_emit' || call.callType === 'async_subscribe');
|
|
29
|
+
const environmentChanged = repositoryEnvironment(db, repoId)
|
|
30
|
+
!== nextEnvironmentJson;
|
|
31
|
+
if (!hasNewEvents && priorEventFactCount(db, repoId) === 0
|
|
32
|
+
&& !environmentChanged) return;
|
|
33
|
+
db.prepare(`UPDATE repositories SET
|
|
34
|
+
graph_stale_reason='event_surface_facts_changed',
|
|
35
|
+
graph_stale_at=datetime('now')
|
|
36
|
+
WHERE workspace_id=(SELECT workspace_id FROM repositories WHERE id=?)`)
|
|
37
|
+
.run(repoId);
|
|
38
|
+
}
|
|
@@ -36,6 +36,14 @@ export const LINK_FACT_JSON_INVENTORY: readonly FactJsonInventoryItem[] = [
|
|
|
36
36
|
consumers: ['package-import-symbol-resolver'],
|
|
37
37
|
repositoryJoin: '',
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
table: 'repositories',
|
|
41
|
+
column: 'environment_declarations_json',
|
|
42
|
+
nullable: false,
|
|
43
|
+
shape: 'object',
|
|
44
|
+
consumers: ['event-environment-resolver'],
|
|
45
|
+
repositoryJoin: '',
|
|
46
|
+
},
|
|
39
47
|
{
|
|
40
48
|
table: 'handler_methods',
|
|
41
49
|
column: 'decorator_resolution_json',
|
|
@@ -77,6 +85,14 @@ export const LINK_FACT_JSON_INVENTORY: readonly FactJsonInventoryItem[] = [
|
|
|
77
85
|
consumers: ['cross-repository-linker', 'trace-engine'],
|
|
78
86
|
repositoryJoin: directRepositoryJoin,
|
|
79
87
|
},
|
|
88
|
+
{
|
|
89
|
+
table: 'outbound_calls',
|
|
90
|
+
column: 'event_skeleton_json',
|
|
91
|
+
nullable: true,
|
|
92
|
+
shape: 'object',
|
|
93
|
+
consumers: ['event-shape-linker', 'event-runtime-resolver'],
|
|
94
|
+
repositoryJoin: directRepositoryJoin,
|
|
95
|
+
},
|
|
80
96
|
{
|
|
81
97
|
table: 'service_bindings',
|
|
82
98
|
column: 'placeholders_json',
|