@saptools/service-flow 0.1.70 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +9 -5
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-GSLFY6J2.js → chunk-Z6D433R5.js} +8696 -6476
- package/dist/chunk-Z6D433R5.js.map +1 -0
- package/dist/cli.js +685 -142
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +53 -14
- package/dist/index.js +2 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +371 -0
- package/src/cli/doctor.ts +4 -6
- package/src/cli.ts +1 -1
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -5
- package/src/db/event-fact-semantics.ts +347 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/migrations.ts +12 -1
- package/src/db/package-target-invalidation.ts +79 -0
- package/src/db/repositories.ts +28 -2
- package/src/db/schema-structure.ts +41 -1
- package/src/db/schema.ts +6 -2
- package/src/indexer/repository-indexer.ts +45 -6
- package/src/linker/cross-repo-linker.ts +25 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +161 -0
- package/src/linker/event-subscription-handler-linker.ts +123 -19
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +298 -0
- package/src/output/table-output.ts +13 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +327 -0
- package/src/parsers/event-call-analysis.ts +242 -0
- package/src/parsers/event-environment-reference.ts +231 -0
- package/src/parsers/event-loop-registration.ts +132 -0
- package/src/parsers/event-name-import-resolution.ts +243 -0
- package/src/parsers/event-receiver-analysis.ts +404 -0
- package/src/parsers/event-subscription-facts.ts +4 -0
- package/src/parsers/generated-constants-parser.ts +80 -14
- package/src/parsers/outbound-call-classifier.ts +27 -124
- package/src/parsers/outbound-call-parser.ts +13 -1
- package/src/parsers/outbound-expression-analysis.ts +2 -2
- package/src/parsers/stable-local-value.ts +30 -9
- package/src/parsers/string-constant-lookups.ts +358 -0
- package/src/trace/event-runtime-resolution.ts +24 -3
- package/src/trace/event-shape-candidate-trace.ts +172 -0
- package/src/trace/event-subscriber-traversal.ts +19 -7
- package/src/trace/evidence.ts +7 -0
- package/src/trace/trace-scope-execution.ts +21 -29
- package/src/types.ts +17 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { Db } from './connection.js';
|
|
2
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
3
|
+
import {
|
|
4
|
+
EVENT_ENVIRONMENT_KEY_ALLOWLIST,
|
|
5
|
+
parseEnvironmentDeclarationsFact,
|
|
6
|
+
} from '../parsers/environment-declarations.js';
|
|
7
|
+
import type {
|
|
8
|
+
EventEnvironmentReference,
|
|
9
|
+
} from '../parsers/event-environment-reference.js';
|
|
10
|
+
import { parseEventSkeletonFact } from '../utils/event-skeleton.js';
|
|
11
|
+
import { parsePackageImportReference } from
|
|
12
|
+
'../parsers/package-fact-contract.js';
|
|
13
|
+
import {
|
|
14
|
+
expectedPackageEventConstantResolution,
|
|
15
|
+
type PackageEventConstantResolution,
|
|
16
|
+
} from '../linker/package-event-constant-resolver.js';
|
|
17
|
+
|
|
18
|
+
export interface EventFactSemanticCategoryCount {
|
|
19
|
+
category: string;
|
|
20
|
+
count: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EventFactRow extends Record<string, unknown> {
|
|
24
|
+
workspaceId?: number;
|
|
25
|
+
eventName?: string;
|
|
26
|
+
skeletonSignature?: string | null;
|
|
27
|
+
skeletonJson?: string | null;
|
|
28
|
+
unresolvedReason?: string | null;
|
|
29
|
+
evidenceJson?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const receiverReasons = new Set([
|
|
33
|
+
'event_receiver_unproven_binding',
|
|
34
|
+
'event_receiver_unproven_propagation',
|
|
35
|
+
'event_receiver_not_cap_client',
|
|
36
|
+
]);
|
|
37
|
+
const constantReasons = new Set([
|
|
38
|
+
'event_name_constant_container_ambiguous',
|
|
39
|
+
'event_name_constant_member_not_string',
|
|
40
|
+
'event_name_constant_container_mutable',
|
|
41
|
+
'event_name_constant_container_not_exported',
|
|
42
|
+
'event_name_constant_resolution_pending',
|
|
43
|
+
]);
|
|
44
|
+
const environmentKeys = new Set<string>(EVENT_ENVIRONMENT_KEY_ALLOWLIST);
|
|
45
|
+
|
|
46
|
+
function category(
|
|
47
|
+
name: string,
|
|
48
|
+
count: number,
|
|
49
|
+
): EventFactSemanticCategoryCount[] {
|
|
50
|
+
return count > 0 ? [{ category: name, count }] : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
54
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
55
|
+
? value as Record<string, unknown> : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function jsonRecord(value: unknown): Record<string, unknown> | undefined {
|
|
59
|
+
if (typeof value !== 'string') return undefined;
|
|
60
|
+
try {
|
|
61
|
+
return record(JSON.parse(value) as unknown);
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function currentEventRows(
|
|
68
|
+
db: Db,
|
|
69
|
+
workspaceId?: number,
|
|
70
|
+
): EventFactRow[] {
|
|
71
|
+
return db.prepare(`SELECT fact.event_name_expr eventName,
|
|
72
|
+
r.workspace_id workspaceId,
|
|
73
|
+
fact.event_skeleton_signature skeletonSignature,
|
|
74
|
+
fact.event_skeleton_json skeletonJson,
|
|
75
|
+
fact.unresolved_reason unresolvedReason,
|
|
76
|
+
fact.evidence_json evidenceJson
|
|
77
|
+
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
78
|
+
WHERE r.fact_analyzer_version=?
|
|
79
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
80
|
+
AND fact.call_type IN ('async_emit','async_subscribe')`).all(
|
|
81
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
82
|
+
) as EventFactRow[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function integerField(
|
|
86
|
+
value: Record<string, unknown>,
|
|
87
|
+
key: string,
|
|
88
|
+
): number | undefined {
|
|
89
|
+
const item = value[key];
|
|
90
|
+
return Number.isInteger(item) && Number(item) >= 0
|
|
91
|
+
? Number(item) : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function packagePendingValid(
|
|
95
|
+
row: EventFactRow,
|
|
96
|
+
evidence: Record<string, unknown>,
|
|
97
|
+
): boolean {
|
|
98
|
+
const source = evidence.eventNameConstantSourceExpression;
|
|
99
|
+
return evidence.eventNameUnresolvedReason
|
|
100
|
+
=== 'event_name_constant_resolution_pending'
|
|
101
|
+
&& evidence.eventNamePackageConstantResolution === undefined
|
|
102
|
+
&& typeof source === 'string' && source.length > 0
|
|
103
|
+
&& row.eventName === source;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolutionFieldsValid(
|
|
107
|
+
value: Record<string, unknown>,
|
|
108
|
+
expected: PackageEventConstantResolution,
|
|
109
|
+
): boolean {
|
|
110
|
+
return value.status === expected.status
|
|
111
|
+
&& value.reason === expected.reason
|
|
112
|
+
&& integerField(value, 'candidateCount') === expected.candidateCount
|
|
113
|
+
&& integerField(value, 'eligibleCandidateCount')
|
|
114
|
+
=== expected.eligibleCandidateCount
|
|
115
|
+
&& integerField(value, 'selectedCandidateCount')
|
|
116
|
+
=== (expected.status === 'resolved' ? 1 : 0)
|
|
117
|
+
&& value.candidateSetComplete === expected.complete
|
|
118
|
+
&& value.resolvedModulePath === expected.modulePath
|
|
119
|
+
&& value.targetRepositoryId === expected.targetRepoId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function packageTerminalValid(
|
|
123
|
+
db: Db,
|
|
124
|
+
row: EventFactRow,
|
|
125
|
+
evidence: Record<string, unknown>,
|
|
126
|
+
): boolean {
|
|
127
|
+
const binding = parsePackageImportReference(
|
|
128
|
+
evidence.eventNameConstantImportBinding,
|
|
129
|
+
);
|
|
130
|
+
const workspaceId = row.workspaceId;
|
|
131
|
+
const resolution = record(evidence.eventNamePackageConstantResolution);
|
|
132
|
+
if (!binding || typeof workspaceId !== 'number' || !resolution)
|
|
133
|
+
return false;
|
|
134
|
+
const expected = expectedPackageEventConstantResolution(
|
|
135
|
+
db, workspaceId, binding,
|
|
136
|
+
);
|
|
137
|
+
const source = evidence.eventNameConstantSourceExpression;
|
|
138
|
+
const expectedName = expected.value ?? source;
|
|
139
|
+
const expectedReason = expected.status === 'resolved'
|
|
140
|
+
? undefined : expected.reason;
|
|
141
|
+
if (row.eventName !== expectedName
|
|
142
|
+
|| evidence.eventNameUnresolvedReason !== expectedReason
|
|
143
|
+
|| !resolutionFieldsValid(resolution, expected)) return false;
|
|
144
|
+
if (expected.status !== 'resolved') return true;
|
|
145
|
+
const constant = record(evidence.eventNameConstant);
|
|
146
|
+
return constant?.sourceKind === 'package_static_string'
|
|
147
|
+
&& constant.sourceFile === expected.sourceFile
|
|
148
|
+
&& constant.sourceLine === expected.sourceLine;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function packageConstantValid(
|
|
152
|
+
db: Db,
|
|
153
|
+
row: EventFactRow,
|
|
154
|
+
evidence: Record<string, unknown>,
|
|
155
|
+
phase: 'pre_package' | 'terminal',
|
|
156
|
+
): boolean {
|
|
157
|
+
if (evidence.eventNameConstantImportBinding === undefined) return true;
|
|
158
|
+
const binding = parsePackageImportReference(
|
|
159
|
+
evidence.eventNameConstantImportBinding,
|
|
160
|
+
);
|
|
161
|
+
const source = evidence.eventNameConstantSourceExpression;
|
|
162
|
+
if (!binding || typeof source !== 'string' || source.length === 0)
|
|
163
|
+
return false;
|
|
164
|
+
if (packagePendingValid(row, evidence))
|
|
165
|
+
return phase === 'pre_package';
|
|
166
|
+
return packageTerminalValid(db, row, evidence);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function receiverEvidenceValid(
|
|
170
|
+
row: EventFactRow,
|
|
171
|
+
evidence: Record<string, unknown>,
|
|
172
|
+
): boolean {
|
|
173
|
+
const classification = evidence.receiverClassification;
|
|
174
|
+
const proof = evidence.receiverProof;
|
|
175
|
+
if (!['cap_evidence', 'name_fallback', 'unproven'].includes(
|
|
176
|
+
String(classification),
|
|
177
|
+
) || typeof proof !== 'string' || proof.length === 0
|
|
178
|
+
|| !Array.isArray(evidence.consideredBindingSites)
|
|
179
|
+
|| evidence.consideredBindingSites.length > 8) return false;
|
|
180
|
+
if (classification === 'name_fallback')
|
|
181
|
+
return proof === 'compatibility_name_fallback';
|
|
182
|
+
if (classification !== 'unproven') return true;
|
|
183
|
+
return typeof row.unresolvedReason === 'string'
|
|
184
|
+
&& receiverReasons.has(row.unresolvedReason);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function environmentBindingValid(
|
|
188
|
+
binding: EventEnvironmentReference,
|
|
189
|
+
sourceKeys: readonly string[],
|
|
190
|
+
): boolean {
|
|
191
|
+
if (!['resolved', 'refused'].includes(binding.status)
|
|
192
|
+
|| !sourceKeys.includes(binding.sourceKey)
|
|
193
|
+
|| !Array.isArray(binding.transforms)
|
|
194
|
+
|| !binding.transforms.every((item) =>
|
|
195
|
+
item === 'toUpperCase' || item === 'toLowerCase')) return false;
|
|
196
|
+
if (binding.status === 'refused')
|
|
197
|
+
return typeof binding.reason === 'string' && binding.reason.length > 0;
|
|
198
|
+
return typeof binding.environmentKey === 'string'
|
|
199
|
+
&& environmentKeys.has(binding.environmentKey)
|
|
200
|
+
&& typeof binding.sourceFile === 'string' && binding.sourceFile.length > 0
|
|
201
|
+
&& Number.isInteger(binding.startOffset)
|
|
202
|
+
&& Number(binding.startOffset) >= 0
|
|
203
|
+
&& Number.isInteger(binding.endOffset)
|
|
204
|
+
&& Number(binding.endOffset) > Number(binding.startOffset);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function skeletonValid(
|
|
208
|
+
row: EventFactRow,
|
|
209
|
+
evidence: Record<string, unknown>,
|
|
210
|
+
): boolean {
|
|
211
|
+
const reason = evidence.eventNameUnresolvedReason;
|
|
212
|
+
if (reason !== 'dynamic_event_name_identifier')
|
|
213
|
+
return row.skeletonSignature == null && row.skeletonJson == null;
|
|
214
|
+
const skeleton = parseEventSkeletonFact(row.skeletonJson);
|
|
215
|
+
if (!skeleton || skeleton.status !== 'complete'
|
|
216
|
+
|| row.skeletonSignature !== skeleton.signature
|
|
217
|
+
|| !skeleton.environmentBindings.every((binding) =>
|
|
218
|
+
environmentBindingValid(binding, skeleton.sourceKeys))) return false;
|
|
219
|
+
const keys = evidence.eventNamePlaceholderKeys;
|
|
220
|
+
return Array.isArray(keys)
|
|
221
|
+
&& JSON.stringify(keys) === JSON.stringify(skeleton.sourceKeys);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function eventReasonValid(
|
|
225
|
+
row: EventFactRow,
|
|
226
|
+
evidence: Record<string, unknown>,
|
|
227
|
+
): boolean {
|
|
228
|
+
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
|
+
if (eventReason === undefined) return row.unresolvedReason == null;
|
|
235
|
+
if (eventReason === 'dynamic_event_name_identifier'
|
|
236
|
+
|| eventReason === 'dynamic_event_name_unsupported_expression'
|
|
237
|
+
|| constantReasons.has(String(eventReason)))
|
|
238
|
+
return row.unresolvedReason === eventReason;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function invalidEventRows(
|
|
243
|
+
db: Db,
|
|
244
|
+
workspaceId?: number,
|
|
245
|
+
phase: 'pre_package' | 'terminal' = 'pre_package',
|
|
246
|
+
): number {
|
|
247
|
+
return currentEventRows(db, workspaceId).filter((row) => {
|
|
248
|
+
if (typeof row.eventName !== 'string' || row.eventName.length === 0)
|
|
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;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function invalidEnvironmentRepositories(
|
|
260
|
+
db: Db,
|
|
261
|
+
workspaceId?: number,
|
|
262
|
+
): number {
|
|
263
|
+
const rows = db.prepare(`SELECT environment_declarations_json value
|
|
264
|
+
FROM repositories WHERE fact_analyzer_version=?
|
|
265
|
+
AND (? IS NULL OR workspace_id=?)`).all(
|
|
266
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
267
|
+
);
|
|
268
|
+
return rows.filter((row) =>
|
|
269
|
+
!parseEnvironmentDeclarationsFact(row.value)).length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function generatedConstantInvalid(row: Record<string, unknown>): boolean {
|
|
273
|
+
const kind = String(row.constantKind);
|
|
274
|
+
const container = row.containerName;
|
|
275
|
+
const member = row.memberName;
|
|
276
|
+
const name = row.name;
|
|
277
|
+
const memberShape = kind === 'const_identifier'
|
|
278
|
+
? container == null && member == null
|
|
279
|
+
: typeof container === 'string' && container.length > 0
|
|
280
|
+
&& typeof member === 'string' && member.length > 0
|
|
281
|
+
&& name === `${container}.${member}`;
|
|
282
|
+
const resolved = row.resolutionStatus === 'resolved';
|
|
283
|
+
const refused = row.resolutionStatus === 'refused';
|
|
284
|
+
return !['const_identifier', 'enum_member', 'const_object_property']
|
|
285
|
+
.includes(kind)
|
|
286
|
+
|| typeof name !== 'string' || name.length === 0
|
|
287
|
+
|| (!resolved && !refused)
|
|
288
|
+
|| (resolved && (typeof row.value !== 'string'
|
|
289
|
+
|| row.value.length === 0 || row.unresolvedReason != null))
|
|
290
|
+
|| (refused && (row.value != null
|
|
291
|
+
|| !constantReasons.has(String(row.unresolvedReason))))
|
|
292
|
+
|| typeof row.sourceFile !== 'string' || row.sourceFile.length === 0
|
|
293
|
+
|| !Number.isInteger(row.sourceLine) || Number(row.sourceLine) < 1
|
|
294
|
+
|| ![0, 1].includes(Number(row.exported))
|
|
295
|
+
|| ![0, 1].includes(Number(row.stable))
|
|
296
|
+
|| !validConstantOffsets(row) || !memberShape;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validConstantOffsets(row: Record<string, unknown>): boolean {
|
|
300
|
+
const start = row.declarationStartOffset;
|
|
301
|
+
const end = row.declarationEndOffset;
|
|
302
|
+
const valueStart = row.valueStartOffset;
|
|
303
|
+
const valueEnd = row.valueEndOffset;
|
|
304
|
+
return Number.isInteger(start) && Number(start) >= 0
|
|
305
|
+
&& Number.isInteger(end) && Number(end) > Number(start)
|
|
306
|
+
&& Number.isInteger(valueStart) && Number(valueStart) >= Number(start)
|
|
307
|
+
&& Number.isInteger(valueEnd) && Number(valueEnd) > Number(valueStart)
|
|
308
|
+
&& Number(valueEnd) <= Number(end);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function invalidGeneratedConstants(
|
|
312
|
+
db: Db,
|
|
313
|
+
workspaceId?: number,
|
|
314
|
+
): number {
|
|
315
|
+
const rows = db.prepare(`SELECT fact.source_file sourceFile,
|
|
316
|
+
fact.source_line sourceLine,fact.name,fact.container_name containerName,
|
|
317
|
+
fact.member_name memberName,fact.value,
|
|
318
|
+
fact.constant_kind constantKind,fact.exported,fact.stable,
|
|
319
|
+
fact.resolution_status resolutionStatus,
|
|
320
|
+
fact.unresolved_reason unresolvedReason,
|
|
321
|
+
fact.declaration_start_offset declarationStartOffset,
|
|
322
|
+
fact.declaration_end_offset declarationEndOffset,
|
|
323
|
+
fact.value_start_offset valueStartOffset,
|
|
324
|
+
fact.value_end_offset valueEndOffset
|
|
325
|
+
FROM generated_constants fact
|
|
326
|
+
JOIN repositories r ON r.id=fact.repo_id
|
|
327
|
+
WHERE r.fact_analyzer_version=?
|
|
328
|
+
AND (? IS NULL OR r.workspace_id=?)`).all(
|
|
329
|
+
ANALYZER_VERSION, workspaceId, workspaceId,
|
|
330
|
+
);
|
|
331
|
+
return rows.filter(generatedConstantInvalid).length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function invalidEventFactCategories(
|
|
335
|
+
db: Db,
|
|
336
|
+
workspaceId?: number,
|
|
337
|
+
phase: 'pre_package' | 'terminal' = 'pre_package',
|
|
338
|
+
): EventFactSemanticCategoryCount[] {
|
|
339
|
+
return [
|
|
340
|
+
...category('event_fact_semantics_invalid',
|
|
341
|
+
invalidEventRows(db, workspaceId, phase)),
|
|
342
|
+
...category('repository_environment_declarations_invalid',
|
|
343
|
+
invalidEnvironmentRepositories(db, workspaceId)),
|
|
344
|
+
...category('generated_constant_fact_invalid',
|
|
345
|
+
invalidGeneratedConstants(db, workspaceId)),
|
|
346
|
+
];
|
|
347
|
+
}
|
|
@@ -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',
|
package/src/db/migrations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Db } from './connection.js';
|
|
2
2
|
import { schemaIndexesSql, schemaTablesSql } from './schema.js';
|
|
3
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
3
|
+
export const CURRENT_SCHEMA_VERSION = 14;
|
|
4
4
|
const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
5
5
|
handler_methods: [
|
|
6
6
|
{ name: 'decorator_resolution_json', ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" },
|
|
@@ -20,6 +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: 'ALTER TABLE repositories ADD COLUMN environment_declarations_json TEXT' },
|
|
23
24
|
],
|
|
24
25
|
graph_edges: [
|
|
25
26
|
{ name: 'status', ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
|
|
@@ -60,6 +61,8 @@ const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
|
60
61
|
{ name: 'external_target_dynamic', ddl: 'ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0' },
|
|
61
62
|
{ name: 'call_site_start_offset', ddl: 'ALTER TABLE outbound_calls ADD COLUMN call_site_start_offset INTEGER' },
|
|
62
63
|
{ name: 'call_site_end_offset', ddl: 'ALTER TABLE outbound_calls ADD COLUMN call_site_end_offset INTEGER' },
|
|
64
|
+
{ name: 'event_skeleton_signature', ddl: 'ALTER TABLE outbound_calls ADD COLUMN event_skeleton_signature TEXT' },
|
|
65
|
+
{ name: 'event_skeleton_json', ddl: 'ALTER TABLE outbound_calls ADD COLUMN event_skeleton_json TEXT' },
|
|
63
66
|
],
|
|
64
67
|
symbol_calls: [
|
|
65
68
|
{ name: 'call_site_start_offset', ddl: 'ALTER TABLE symbol_calls ADD COLUMN call_site_start_offset INTEGER' },
|
|
@@ -104,6 +107,13 @@ function markFactProvenanceMigrationStale(db: Db, priorVersion: number): void {
|
|
|
104
107
|
graph_stale_at=COALESCE(graph_stale_at,datetime('now'))
|
|
105
108
|
WHERE index_status='indexed' OR last_indexed_at IS NOT NULL`).run();
|
|
106
109
|
}
|
|
110
|
+
function markEventSurfaceMigrationStale(db: Db, priorVersion: number): void {
|
|
111
|
+
if (priorVersion >= 14) return;
|
|
112
|
+
db.prepare(`UPDATE repositories
|
|
113
|
+
SET graph_stale_reason='schema_v14_event_surface_requires_reindex',
|
|
114
|
+
graph_stale_at=COALESCE(graph_stale_at,datetime('now'))
|
|
115
|
+
WHERE index_status='indexed' OR last_indexed_at IS NOT NULL`).run();
|
|
116
|
+
}
|
|
107
117
|
export function migrate(db: Db): void {
|
|
108
118
|
db.transaction(() => {
|
|
109
119
|
const version = userVersion(db);
|
|
@@ -114,6 +124,7 @@ export function migrate(db: Db): void {
|
|
|
114
124
|
normalizeLegacyStatus(db, version);
|
|
115
125
|
markCallSiteMigrationStale(db, version);
|
|
116
126
|
markFactProvenanceMigrationStale(db, version);
|
|
127
|
+
markEventSurfaceMigrationStale(db, version);
|
|
117
128
|
const violations = db.pragma('foreign_key_check');
|
|
118
129
|
if (violations.length > 0) throw new Error('SQLite foreign_key_check failed during migration');
|
|
119
130
|
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
@@ -10,6 +10,13 @@ interface PackageCallRow {
|
|
|
10
10
|
evidence: Record<string, unknown>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface PackageEventCallRow {
|
|
14
|
+
id: number;
|
|
15
|
+
repoId: number;
|
|
16
|
+
unresolvedReason?: string | null;
|
|
17
|
+
evidence: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export interface PackageInvalidationBatch {
|
|
14
21
|
publishingRepoIds: ReadonlySet<number>;
|
|
15
22
|
affectedCallerRepoIds: Set<number>;
|
|
@@ -44,6 +51,14 @@ function packageName(evidence: Record<string, unknown>): string | undefined {
|
|
|
44
51
|
?.requestedPackageName ?? undefined;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
function eventPackageName(
|
|
55
|
+
evidence: Record<string, unknown>,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
return parsePackageImportReference(
|
|
58
|
+
evidence.eventNameConstantImportBinding,
|
|
59
|
+
)?.requestedPackageName ?? undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function packageCallEvidenceValid(
|
|
48
63
|
evidence: Record<string, unknown>,
|
|
49
64
|
): boolean {
|
|
@@ -94,6 +109,67 @@ function pendingEvidence(evidence: Record<string, unknown>): string {
|
|
|
94
109
|
});
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
function currentEventCalls(
|
|
113
|
+
db: Db,
|
|
114
|
+
workspaceId: number,
|
|
115
|
+
targetRepoId: number,
|
|
116
|
+
): PackageEventCallRow[] {
|
|
117
|
+
const rows = db.prepare(`SELECT c.id,c.repo_id repoId,
|
|
118
|
+
c.unresolved_reason unresolvedReason,c.evidence_json evidenceJson
|
|
119
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
120
|
+
WHERE r.workspace_id=? AND r.id<>? AND r.fact_analyzer_version=?
|
|
121
|
+
AND json_extract(c.evidence_json,
|
|
122
|
+
'$.eventNameConstantImportBinding.moduleKind')='package'
|
|
123
|
+
ORDER BY c.id`).all(workspaceId, targetRepoId, ANALYZER_VERSION);
|
|
124
|
+
return rows.flatMap((row) => {
|
|
125
|
+
const evidence = parsedEvidence(row.evidenceJson);
|
|
126
|
+
return evidence && eventPackageName(evidence)
|
|
127
|
+
&& typeof row.id === 'number' && typeof row.repoId === 'number'
|
|
128
|
+
? [{
|
|
129
|
+
id: row.id, repoId: row.repoId, evidence,
|
|
130
|
+
unresolvedReason: typeof row.unresolvedReason === 'string'
|
|
131
|
+
? row.unresolvedReason : null,
|
|
132
|
+
}] : [];
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pendingEventEvidence(
|
|
137
|
+
evidence: Record<string, unknown>,
|
|
138
|
+
): string {
|
|
139
|
+
const parser = { ...evidence };
|
|
140
|
+
delete parser.eventNameConstant;
|
|
141
|
+
delete parser.eventNamePackageConstantResolution;
|
|
142
|
+
parser.eventNameUnresolvedReason = 'event_name_constant_resolution_pending';
|
|
143
|
+
parser.eventNameStatus = 'dynamic';
|
|
144
|
+
parser.eventNameSourceKind = 'dynamic_expression';
|
|
145
|
+
parser.eventNamePlaceholderKeys = [];
|
|
146
|
+
return JSON.stringify(parser);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resetPackageEventCalls(
|
|
150
|
+
db: Db,
|
|
151
|
+
workspaceId: number,
|
|
152
|
+
targetRepoId: number,
|
|
153
|
+
names: ReadonlySet<string>,
|
|
154
|
+
batch: PackageInvalidationBatch,
|
|
155
|
+
): boolean {
|
|
156
|
+
const update = db.prepare(`UPDATE outbound_calls SET event_name_expr=?,
|
|
157
|
+
unresolved_reason=?,evidence_json=? WHERE id=?`);
|
|
158
|
+
let matched = false;
|
|
159
|
+
for (const call of currentEventCalls(db, workspaceId, targetRepoId)) {
|
|
160
|
+
if (!names.has(eventPackageName(call.evidence) ?? '')) continue;
|
|
161
|
+
const source = call.evidence.eventNameConstantSourceExpression;
|
|
162
|
+
if (typeof source !== 'string' || source.length === 0)
|
|
163
|
+
throw new Error('invalid_current_package_event_constant_evidence');
|
|
164
|
+
const reason = call.evidence.receiverClassification === 'unproven'
|
|
165
|
+
? call.unresolvedReason : 'event_name_constant_resolution_pending';
|
|
166
|
+
update.run(source, reason, pendingEventEvidence(call.evidence), call.id);
|
|
167
|
+
batch.affectedCallerRepoIds.add(call.repoId);
|
|
168
|
+
matched = true;
|
|
169
|
+
}
|
|
170
|
+
return matched;
|
|
171
|
+
}
|
|
172
|
+
|
|
97
173
|
function targetWorkspace(
|
|
98
174
|
db: Db,
|
|
99
175
|
repoId: number,
|
|
@@ -142,6 +218,9 @@ export function invalidatePackageTargetFacts(
|
|
|
142
218
|
batch.affectedCallerRepoIds.add(call.repoId);
|
|
143
219
|
matched = true;
|
|
144
220
|
}
|
|
221
|
+
matched = resetPackageEventCalls(
|
|
222
|
+
db, target.workspaceId, targetRepoId, names, batch,
|
|
223
|
+
) || matched;
|
|
145
224
|
if (matched || packageIdentityChanged(
|
|
146
225
|
target.packageName, newPackageName,
|
|
147
226
|
)) batch.affectedWorkspaceIds.add(target.workspaceId);
|
package/src/db/repositories.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
HandlerRegistrationFact,
|
|
8
8
|
ServiceBindingFact,
|
|
9
9
|
ExecutableSymbolFact,
|
|
10
|
+
GeneratedConstantFact,
|
|
10
11
|
} from '../types.js';
|
|
11
12
|
import {
|
|
12
13
|
selectCallOwner,
|
|
@@ -16,6 +17,9 @@ import {
|
|
|
16
17
|
PreparedRepositorySnapshotError,
|
|
17
18
|
type PreparedSnapshotFailureCode,
|
|
18
19
|
} from './index-publication-failure.js';
|
|
20
|
+
import {
|
|
21
|
+
emptyEnvironmentDeclarations,
|
|
22
|
+
} from '../parsers/environment-declarations.js';
|
|
19
23
|
export interface RepoRow {
|
|
20
24
|
id: number;
|
|
21
25
|
name: string;
|
|
@@ -92,8 +96,8 @@ export function upsertRepository(
|
|
|
92
96
|
db.prepare(
|
|
93
97
|
`INSERT INTO repositories(workspace_id,name,absolute_path,relative_path,
|
|
94
98
|
package_name,package_version,dependencies_json,
|
|
95
|
-
package_public_surface_json,kind,is_git_repo)
|
|
96
|
-
VALUES(
|
|
99
|
+
package_public_surface_json,environment_declarations_json,kind,is_git_repo)
|
|
100
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
97
101
|
ON CONFLICT(workspace_id,absolute_path) DO UPDATE SET
|
|
98
102
|
name=excluded.name,relative_path=excluded.relative_path,
|
|
99
103
|
package_name=excluded.package_name,
|
|
@@ -108,6 +112,7 @@ export function upsertRepository(
|
|
|
108
112
|
r.packageVersion,
|
|
109
113
|
JSON.stringify(r.dependencies ?? {}),
|
|
110
114
|
initialPackagePublicSurface(r.packageName),
|
|
115
|
+
JSON.stringify(emptyEnvironmentDeclarations()),
|
|
111
116
|
r.kind ?? 'unknown',
|
|
112
117
|
r.isGitRepo ? 1 : 0,
|
|
113
118
|
);
|
|
@@ -152,6 +157,7 @@ export function clearRepoFacts(db: Db, repoId: number): void {
|
|
|
152
157
|
'symbol_calls',
|
|
153
158
|
'handler_registrations',
|
|
154
159
|
'service_bindings',
|
|
160
|
+
'generated_constants',
|
|
155
161
|
'symbols',
|
|
156
162
|
'diagnostics',
|
|
157
163
|
'files',
|
|
@@ -159,6 +165,26 @@ export function clearRepoFacts(db: Db, repoId: number): void {
|
|
|
159
165
|
db.prepare(`DELETE FROM ${t} WHERE repo_id=?`).run(repoId);
|
|
160
166
|
db.prepare('DELETE FROM search_index WHERE repo=?').run(String(repoId));
|
|
161
167
|
}
|
|
168
|
+
export function insertGeneratedConstants(
|
|
169
|
+
db: Db,
|
|
170
|
+
repoId: number,
|
|
171
|
+
rows: GeneratedConstantFact[],
|
|
172
|
+
): void {
|
|
173
|
+
const stmt = db.prepare(`INSERT INTO generated_constants(
|
|
174
|
+
repo_id,source_file,source_line,name,container_name,member_name,value,
|
|
175
|
+
constant_kind,exported,stable,resolution_status,unresolved_reason,
|
|
176
|
+
declaration_start_offset,declaration_end_offset,
|
|
177
|
+
value_start_offset,value_end_offset
|
|
178
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
179
|
+
for (const row of rows) stmt.run(
|
|
180
|
+
repoId, row.sourceFile, row.sourceLine, row.name,
|
|
181
|
+
row.containerName ?? null, row.memberName ?? null, row.value ?? null,
|
|
182
|
+
row.constantKind, row.exported ? 1 : 0, row.stable ? 1 : 0,
|
|
183
|
+
row.resolutionStatus, row.unresolvedReason ?? null,
|
|
184
|
+
row.declarationStartOffset, row.declarationEndOffset,
|
|
185
|
+
row.valueStartOffset, row.valueEndOffset,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
162
188
|
export function insertRequires(
|
|
163
189
|
db: Db,
|
|
164
190
|
repoId: number,
|