@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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Db } from './connection.js';
|
|
2
2
|
import { ANALYZER_VERSION } from '../version.js';
|
|
3
3
|
import {
|
|
4
|
-
EVENT_ENVIRONMENT_KEY_ALLOWLIST,
|
|
5
4
|
parseEnvironmentDeclarationsFact,
|
|
6
5
|
} from '../parsers/environment-declarations.js';
|
|
7
6
|
import type {
|
|
@@ -21,12 +20,28 @@ export interface EventFactSemanticCategoryCount {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
interface EventFactRow extends Record<string, unknown> {
|
|
23
|
+
id?: number;
|
|
24
|
+
repoId?: number;
|
|
25
|
+
repositoryName?: string;
|
|
24
26
|
workspaceId?: number;
|
|
27
|
+
sourceFile?: string;
|
|
28
|
+
sourceLine?: number;
|
|
25
29
|
eventName?: string;
|
|
26
30
|
skeletonSignature?: string | null;
|
|
27
31
|
skeletonJson?: string | null;
|
|
28
32
|
unresolvedReason?: string | null;
|
|
29
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;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
const receiverReasons = new Set([
|
|
@@ -38,11 +53,12 @@ const constantReasons = new Set([
|
|
|
38
53
|
'event_name_constant_container_ambiguous',
|
|
39
54
|
'event_name_constant_member_not_string',
|
|
40
55
|
'event_name_constant_container_mutable',
|
|
56
|
+
'event_name_constant_container_unsafe_reference',
|
|
57
|
+
'event_name_constant_container_unsupported_shape',
|
|
41
58
|
'event_name_constant_container_not_exported',
|
|
42
59
|
'event_name_constant_resolution_pending',
|
|
60
|
+
'event_name_constant_value_empty',
|
|
43
61
|
]);
|
|
44
|
-
const environmentKeys = new Set<string>(EVENT_ENVIRONMENT_KEY_ALLOWLIST);
|
|
45
|
-
|
|
46
62
|
function category(
|
|
47
63
|
name: string,
|
|
48
64
|
count: number,
|
|
@@ -68,12 +84,15 @@ function currentEventRows(
|
|
|
68
84
|
db: Db,
|
|
69
85
|
workspaceId?: number,
|
|
70
86
|
): EventFactRow[] {
|
|
71
|
-
return db.prepare(`SELECT fact.
|
|
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,
|
|
72
90
|
r.workspace_id workspaceId,
|
|
73
91
|
fact.event_skeleton_signature skeletonSignature,
|
|
74
92
|
fact.event_skeleton_json skeletonJson,
|
|
75
93
|
fact.unresolved_reason unresolvedReason,
|
|
76
|
-
fact.evidence_json evidenceJson
|
|
94
|
+
fact.evidence_json evidenceJson,
|
|
95
|
+
r.environment_declarations_json environmentJson
|
|
77
96
|
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
78
97
|
WHERE r.fact_analyzer_version=?
|
|
79
98
|
AND (? IS NULL OR r.workspace_id=?)
|
|
@@ -82,6 +101,25 @@ function currentEventRows(
|
|
|
82
101
|
) as EventFactRow[];
|
|
83
102
|
}
|
|
84
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
|
+
|
|
85
123
|
function integerField(
|
|
86
124
|
value: Record<string, unknown>,
|
|
87
125
|
key: string,
|
|
@@ -167,37 +205,70 @@ function packageConstantValid(
|
|
|
167
205
|
}
|
|
168
206
|
|
|
169
207
|
function receiverEvidenceValid(
|
|
170
|
-
row: EventFactRow,
|
|
171
208
|
evidence: Record<string, unknown>,
|
|
172
209
|
): boolean {
|
|
173
210
|
const classification = evidence.receiverClassification;
|
|
174
211
|
const proof = evidence.receiverProof;
|
|
175
|
-
if (!
|
|
176
|
-
|
|
177
|
-
) || typeof proof !== 'string' || proof.length === 0
|
|
178
|
-
|| !Array.isArray(evidence.consideredBindingSites)
|
|
179
|
-
|| evidence.consideredBindingSites.length > 8) return false;
|
|
212
|
+
if (!receiverEvidenceShapeValid(classification, proof, evidence))
|
|
213
|
+
return false;
|
|
180
214
|
if (classification === 'name_fallback')
|
|
181
|
-
return proof
|
|
182
|
-
if (classification !== 'unproven')
|
|
183
|
-
|
|
184
|
-
|
|
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;
|
|
185
244
|
}
|
|
186
245
|
|
|
187
246
|
function environmentBindingValid(
|
|
188
247
|
binding: EventEnvironmentReference,
|
|
189
248
|
sourceKeys: readonly string[],
|
|
249
|
+
allowedKeys: ReadonlySet<string>,
|
|
190
250
|
): boolean {
|
|
191
251
|
if (!['resolved', 'refused'].includes(binding.status)
|
|
192
252
|
|| !sourceKeys.includes(binding.sourceKey)
|
|
193
|
-
|| !
|
|
194
|
-
|| !binding.transforms.every((item) =>
|
|
195
|
-
item === 'toUpperCase' || item === 'toLowerCase')) return false;
|
|
253
|
+
|| !environmentTransformsValid(binding.transforms)) return false;
|
|
196
254
|
if (binding.status === 'refused')
|
|
197
255
|
return typeof binding.reason === 'string' && binding.reason.length > 0;
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
201
272
|
&& Number.isInteger(binding.startOffset)
|
|
202
273
|
&& Number(binding.startOffset) >= 0
|
|
203
274
|
&& Number.isInteger(binding.endOffset)
|
|
@@ -212,10 +283,14 @@ function skeletonValid(
|
|
|
212
283
|
if (reason !== 'dynamic_event_name_identifier')
|
|
213
284
|
return row.skeletonSignature == null && row.skeletonJson == null;
|
|
214
285
|
const skeleton = parseEventSkeletonFact(row.skeletonJson);
|
|
286
|
+
const environment = parseEnvironmentDeclarationsFact(row.environmentJson);
|
|
215
287
|
if (!skeleton || skeleton.status !== 'complete'
|
|
288
|
+
|| !environment
|
|
216
289
|
|| row.skeletonSignature !== skeleton.signature
|
|
217
290
|
|| !skeleton.environmentBindings.every((binding) =>
|
|
218
|
-
environmentBindingValid(
|
|
291
|
+
environmentBindingValid(
|
|
292
|
+
binding, skeleton.sourceKeys, new Set(environment.allowedKeys),
|
|
293
|
+
))) return false;
|
|
219
294
|
const keys = evidence.eventNamePlaceholderKeys;
|
|
220
295
|
return Array.isArray(keys)
|
|
221
296
|
&& JSON.stringify(keys) === JSON.stringify(skeleton.sourceKeys);
|
|
@@ -226,11 +301,6 @@ function eventReasonValid(
|
|
|
226
301
|
evidence: Record<string, unknown>,
|
|
227
302
|
): boolean {
|
|
228
303
|
const eventReason = evidence.eventNameUnresolvedReason;
|
|
229
|
-
if (receiverReasons.has(String(row.unresolvedReason)))
|
|
230
|
-
return eventReason === undefined
|
|
231
|
-
|| eventReason === 'dynamic_event_name_identifier'
|
|
232
|
-
|| eventReason === 'dynamic_event_name_unsupported_expression'
|
|
233
|
-
|| constantReasons.has(String(eventReason));
|
|
234
304
|
if (eventReason === undefined) return row.unresolvedReason == null;
|
|
235
305
|
if (eventReason === 'dynamic_event_name_identifier'
|
|
236
306
|
|| eventReason === 'dynamic_event_name_unsupported_expression'
|
|
@@ -244,16 +314,8 @@ function invalidEventRows(
|
|
|
244
314
|
workspaceId?: number,
|
|
245
315
|
phase: 'pre_package' | 'terminal' = 'pre_package',
|
|
246
316
|
): number {
|
|
247
|
-
return currentEventRows(db, workspaceId).filter((row) =>
|
|
248
|
-
|
|
249
|
-
return false;
|
|
250
|
-
const evidence = jsonRecord(row.evidenceJson);
|
|
251
|
-
return !evidence
|
|
252
|
-
|| !receiverEvidenceValid(row, evidence)
|
|
253
|
-
|| !eventReasonValid(row, evidence)
|
|
254
|
-
|| !packageConstantValid(db, row, evidence, phase)
|
|
255
|
-
|| !skeletonValid(row, evidence);
|
|
256
|
-
}).length;
|
|
317
|
+
return currentEventRows(db, workspaceId).filter((row) =>
|
|
318
|
+
eventInvalidPredicate(db, row, phase) !== undefined).length;
|
|
257
319
|
}
|
|
258
320
|
|
|
259
321
|
function invalidEnvironmentRepositories(
|
|
@@ -269,31 +331,59 @@ function invalidEnvironmentRepositories(
|
|
|
269
331
|
!parseEnvironmentDeclarationsFact(row.value)).length;
|
|
270
332
|
}
|
|
271
333
|
|
|
272
|
-
function
|
|
334
|
+
function generatedConstantInvalidPredicate(
|
|
335
|
+
row: Record<string, unknown>,
|
|
336
|
+
): string | undefined {
|
|
273
337
|
const kind = String(row.constantKind);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 {
|
|
282
354
|
const resolved = row.resolutionStatus === 'resolved';
|
|
283
355
|
const refused = row.resolutionStatus === 'refused';
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
||
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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}`;
|
|
297
387
|
}
|
|
298
388
|
|
|
299
389
|
function validConstantOffsets(row: Record<string, unknown>): boolean {
|
|
@@ -312,7 +402,17 @@ function invalidGeneratedConstants(
|
|
|
312
402
|
db: Db,
|
|
313
403
|
workspaceId?: number,
|
|
314
404
|
): number {
|
|
315
|
-
const rows = db
|
|
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,
|
|
316
416
|
fact.source_line sourceLine,fact.name,fact.container_name containerName,
|
|
317
417
|
fact.member_name memberName,fact.value,
|
|
318
418
|
fact.constant_kind constantKind,fact.exported,fact.stable,
|
|
@@ -328,7 +428,106 @@ function invalidGeneratedConstants(
|
|
|
328
428
|
AND (? IS NULL OR r.workspace_id=?)`).all(
|
|
329
429
|
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
330
430
|
);
|
|
331
|
-
|
|
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
|
+
};
|
|
332
531
|
}
|
|
333
532
|
|
|
334
533
|
export function invalidEventFactCategories(
|
|
@@ -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
|
+
}
|
package/src/db/fact-lifecycle.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type SchemaStructureCategoryCount,
|
|
15
15
|
} from './schema-structure.js';
|
|
16
16
|
import { ANALYZER_VERSION } from '../version.js';
|
|
17
|
+
import { invalidEventFactExamples } from './event-fact-semantics.js';
|
|
17
18
|
|
|
18
19
|
export type FactLifecycleCode =
|
|
19
20
|
| 'schema_upgrade_required'
|
|
@@ -27,11 +28,8 @@ export interface FactLifecycleDiagnostic extends Record<string, unknown> {
|
|
|
27
28
|
remediation: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const remediation = [
|
|
31
|
-
'service-flow index --workspace /workspace --force',
|
|
32
|
-
'service-flow link --workspace /workspace --force',
|
|
33
|
-
].join('\n');
|
|
34
31
|
const CATEGORY_LIMIT = 24;
|
|
32
|
+
const EXAMPLE_LIMIT = 5;
|
|
35
33
|
|
|
36
34
|
function count(db: Db, sql: string, ...params: unknown[]): number {
|
|
37
35
|
const row = db.prepare(sql).get(...params);
|
|
@@ -51,12 +49,13 @@ export function factLifecycleDiagnostic(
|
|
|
51
49
|
workspaceId?: number,
|
|
52
50
|
phase: PackageFactPhase = 'pre_package',
|
|
53
51
|
): FactLifecycleDiagnostic | undefined {
|
|
54
|
-
return schemaLifecycleDiagnostic(db)
|
|
52
|
+
return schemaLifecycleDiagnostic(db, workspaceId)
|
|
55
53
|
?? currentFactLifecycleDiagnostic(db, workspaceId, phase);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
export function schemaLifecycleDiagnostic(
|
|
59
57
|
db: Db,
|
|
58
|
+
workspaceId?: number,
|
|
60
59
|
): FactLifecycleDiagnostic | undefined {
|
|
61
60
|
const currentSchema = schemaVersion(db);
|
|
62
61
|
if (currentSchema > CURRENT_SCHEMA_VERSION) return {
|
|
@@ -71,13 +70,15 @@ export function schemaLifecycleDiagnostic(
|
|
|
71
70
|
severity: 'error',
|
|
72
71
|
code: 'schema_upgrade_required',
|
|
73
72
|
message: `Database schema ${currentSchema} must be upgraded to ${CURRENT_SCHEMA_VERSION} before this command can read current call-site facts.`,
|
|
74
|
-
remediation,
|
|
73
|
+
remediation: staleRemediation(db, workspaceId),
|
|
75
74
|
currentSchemaVersion: currentSchema,
|
|
76
75
|
requiredSchemaVersion: CURRENT_SCHEMA_VERSION,
|
|
77
76
|
};
|
|
78
77
|
const structureCategories = invalidSchemaStructureCategories(db);
|
|
79
78
|
if (structureCategories.length > 0)
|
|
80
|
-
return reindexDiagnostic(
|
|
79
|
+
return reindexDiagnostic(
|
|
80
|
+
db, workspaceId, 0, structureCategories, 'pre_package', false,
|
|
81
|
+
);
|
|
81
82
|
return undefined;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -88,15 +89,17 @@ export function currentFactLifecycleDiagnostic(
|
|
|
88
89
|
): FactLifecycleDiagnostic | undefined {
|
|
89
90
|
const staleRepositories = oldAnalyzerCount(db, workspaceId);
|
|
90
91
|
if (staleRepositories > 0)
|
|
91
|
-
return reindexDiagnostic(
|
|
92
|
+
return reindexDiagnostic(
|
|
93
|
+
db, workspaceId, staleRepositories, [], phase,
|
|
94
|
+
);
|
|
92
95
|
const jsonCategories = invalidFactJsonCategories(db, workspaceId);
|
|
93
96
|
if (jsonCategories.length > 0)
|
|
94
|
-
return reindexDiagnostic(0, jsonCategories);
|
|
97
|
+
return reindexDiagnostic(db, workspaceId, 0, jsonCategories, phase);
|
|
95
98
|
const semanticCategories = invalidFactSemanticCategories(
|
|
96
99
|
db, workspaceId, phase,
|
|
97
100
|
);
|
|
98
101
|
if (semanticCategories.length === 0) return undefined;
|
|
99
|
-
return reindexDiagnostic(0, semanticCategories);
|
|
102
|
+
return reindexDiagnostic(db, workspaceId, 0, semanticCategories, phase);
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
type InvalidFactCategory =
|
|
@@ -105,26 +108,81 @@ type InvalidFactCategory =
|
|
|
105
108
|
| SchemaStructureCategoryCount;
|
|
106
109
|
|
|
107
110
|
function reindexDiagnostic(
|
|
111
|
+
db: Db,
|
|
112
|
+
workspaceId: number | undefined,
|
|
108
113
|
staleRepositories: number,
|
|
109
114
|
categories: InvalidFactCategory[],
|
|
115
|
+
phase: PackageFactPhase,
|
|
116
|
+
examplesAllowed = true,
|
|
110
117
|
): FactLifecycleDiagnostic {
|
|
111
118
|
const invalidFacts = categories.reduce((sum, item) => sum + item.count, 0);
|
|
112
119
|
const shown = categories.slice(0, CATEGORY_LIMIT);
|
|
120
|
+
const eventExamples = examplesAllowed
|
|
121
|
+
? invalidEventFactExamples(db, workspaceId, phase, EXAMPLE_LIMIT)
|
|
122
|
+
: { total: 0, affectedRepositoryCount: 0, examples: [] };
|
|
123
|
+
const examples = invalidFacts > 0 ? eventExamples.examples : [];
|
|
113
124
|
return {
|
|
114
125
|
severity: 'error',
|
|
115
126
|
code: 'reindex_required',
|
|
116
|
-
message:
|
|
117
|
-
|
|
127
|
+
message: invalidFacts > 0
|
|
128
|
+
? 'Current facts fail bounded semantic integrity checks; inspect the offending rows before rebuilding graph edges.'
|
|
129
|
+
: 'Current facts are stale; force index and link before tracing or rebuilding graph edges.',
|
|
130
|
+
remediation: invalidFacts > 0
|
|
131
|
+
? invalidFactRemediation(db, workspaceId)
|
|
132
|
+
: staleRemediation(db, workspaceId),
|
|
118
133
|
staleRepositoryCount: staleRepositories,
|
|
119
134
|
invalidCallFactCount: invalidFacts,
|
|
120
135
|
invalidFactCategories: shown,
|
|
121
136
|
invalidFactCategoryCount: categories.length,
|
|
122
137
|
shownInvalidFactCategoryCount: shown.length,
|
|
123
138
|
omittedInvalidFactCategoryCount: categories.length - shown.length,
|
|
139
|
+
affectedRepositoryCount: eventExamples.affectedRepositoryCount,
|
|
140
|
+
workspaceRepositoryCount: repositoryCount(db, workspaceId),
|
|
141
|
+
invalidFactExamples: examples,
|
|
142
|
+
invalidFactExampleCount: eventExamples.total,
|
|
143
|
+
shownInvalidFactExampleCount: examples.length,
|
|
144
|
+
omittedInvalidFactExampleCount:
|
|
145
|
+
Math.max(0, eventExamples.total - examples.length),
|
|
124
146
|
requiredAnalyzerVersion: ANALYZER_VERSION,
|
|
125
147
|
};
|
|
126
148
|
}
|
|
127
149
|
|
|
150
|
+
function workspacePath(db: Db, workspaceId?: number): string | undefined {
|
|
151
|
+
if (workspaceId === undefined) return undefined;
|
|
152
|
+
const row = db.prepare(
|
|
153
|
+
'SELECT root_path rootPath FROM workspaces WHERE id=?',
|
|
154
|
+
).get(workspaceId);
|
|
155
|
+
return typeof row?.rootPath === 'string' ? row.rootPath : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function shellQuote(value: string): string {
|
|
159
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function workspaceArgument(db: Db, workspaceId?: number): string {
|
|
163
|
+
const root = workspacePath(db, workspaceId);
|
|
164
|
+
return root ? ` --workspace ${shellQuote(root)}` : '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function staleRemediation(db: Db, workspaceId?: number): string {
|
|
168
|
+
const workspace = workspaceArgument(db, workspaceId);
|
|
169
|
+
return [
|
|
170
|
+
`service-flow index${workspace} --force`,
|
|
171
|
+
`service-flow link${workspace} --force`,
|
|
172
|
+
].join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function invalidFactRemediation(db: Db, workspaceId?: number): string {
|
|
176
|
+
return `service-flow doctor${workspaceArgument(
|
|
177
|
+
db, workspaceId,
|
|
178
|
+
)} --strict --detail`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function repositoryCount(db: Db, workspaceId?: number): number {
|
|
182
|
+
return count(db, `SELECT COUNT(*) count FROM repositories
|
|
183
|
+
WHERE (? IS NULL OR workspace_id=?)`, workspaceId, workspaceId);
|
|
184
|
+
}
|
|
185
|
+
|
|
128
186
|
export function assertWorkspaceLinkable(
|
|
129
187
|
db: Db,
|
|
130
188
|
workspaceId: number,
|
package/src/db/migrations.ts
CHANGED
|
@@ -20,7 +20,7 @@ const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
|
20
20
|
{ name: 'graph_stale_at', ddl: 'ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT' },
|
|
21
21
|
{ name: 'fact_analyzer_version', ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" },
|
|
22
22
|
{ name: 'package_public_surface_json', ddl: 'ALTER TABLE repositories ADD COLUMN package_public_surface_json TEXT' },
|
|
23
|
-
{ name: 'environment_declarations_json', ddl:
|
|
23
|
+
{ name: 'environment_declarations_json', ddl: `ALTER TABLE repositories ADD COLUMN 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":[]}'` },
|
|
24
24
|
],
|
|
25
25
|
graph_edges: [
|
|
26
26
|
{ name: 'status', ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
|
|
@@ -109,6 +109,9 @@ function markFactProvenanceMigrationStale(db: Db, priorVersion: number): void {
|
|
|
109
109
|
}
|
|
110
110
|
function markEventSurfaceMigrationStale(db: Db, priorVersion: number): void {
|
|
111
111
|
if (priorVersion >= 14) return;
|
|
112
|
+
db.prepare(
|
|
113
|
+
'UPDATE repositories SET environment_declarations_json=NULL',
|
|
114
|
+
).run();
|
|
112
115
|
db.prepare(`UPDATE repositories
|
|
113
116
|
SET graph_stale_reason='schema_v14_event_surface_requires_reindex',
|
|
114
117
|
graph_stale_at=COALESCE(graph_stale_at,datetime('now'))
|