@saptools/service-flow 0.1.67 → 0.1.69
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 +20 -0
- package/README.md +27 -9
- package/TECHNICAL-NOTE.md +36 -0
- package/dist/chunk-3N3B5KHV.js +19596 -0
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +2645 -521
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +67 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +499 -340
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +699 -0
- package/src/db/004-package-target-invalidation.ts +183 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +464 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +210 -0
- package/src/db/009-binding-fact-semantics.ts +352 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +268 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +130 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +135 -13
- package/src/indexer/workspace-indexer.ts +237 -34
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +34 -11
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/006-event-template-link.ts +72 -0
- package/src/linker/007-call-edge-insertion.ts +568 -0
- package/src/linker/cross-repo-linker.ts +4 -166
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/000-direct-query-execution.ts +11 -0
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +348 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +306 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +168 -0
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +162 -512
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +513 -352
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/007-implementation-start-diagnostic.ts +1 -0
- package/src/trace/011-event-subscriber-traversal.ts +100 -8
- package/src/trace/013-trace-root-scopes.ts +2 -3
- package/src/trace/014-compact-contract.ts +6 -0
- package/src/trace/015-trace-edge-recorder.ts +61 -4
- package/src/trace/016-compact-projector.ts +15 -17
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +122 -38
- package/src/trace/021-compact-decision-normalization.ts +171 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +81 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +336 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/030-event-runtime-resolution.ts +151 -0
- package/src/trace/031-local-call-expansion.ts +37 -0
- package/src/trace/implementation-hints.ts +9 -6
- package/src/trace/selectors.ts +1 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +57 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- package/dist/chunk-ZQABU7MR.js +0 -12151
- package/dist/chunk-ZQABU7MR.js.map +0 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
2
|
+
import {
|
|
3
|
+
PACKAGE_PUBLIC_SURFACE_RECORD_CAP,
|
|
4
|
+
PACKAGE_PUBLIC_SURFACE_SCHEMA,
|
|
5
|
+
type ExecutableBodyEligibility,
|
|
6
|
+
type PackagePublicScope,
|
|
7
|
+
type PackagePublicSurfaceFact,
|
|
8
|
+
type PublicSurfaceTarget,
|
|
9
|
+
} from '../parsers/003-package-public-surface.js';
|
|
10
|
+
import {
|
|
11
|
+
parsePackagePublicSurfaceFact,
|
|
12
|
+
} from '../parsers/012-package-fact-contract.js';
|
|
13
|
+
import type { Db } from './connection.js';
|
|
14
|
+
|
|
15
|
+
interface Exposure {
|
|
16
|
+
entry: string;
|
|
17
|
+
modulePath: string;
|
|
18
|
+
publicName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SymbolSidecar {
|
|
22
|
+
body: ExecutableBodyEligibility;
|
|
23
|
+
exposures: Exposure[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SurfaceSymbol {
|
|
27
|
+
id: number;
|
|
28
|
+
repoId: number;
|
|
29
|
+
carrierValid: boolean;
|
|
30
|
+
sourceFile: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
qualifiedName: string;
|
|
33
|
+
startOffset: number;
|
|
34
|
+
endOffset: number;
|
|
35
|
+
evidence?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SurfaceRepository {
|
|
39
|
+
id: number;
|
|
40
|
+
surface?: PackagePublicSurfaceFact;
|
|
41
|
+
symbols: SurfaceSymbol[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const bodyReasons = new Set([
|
|
45
|
+
'body_present', 'declaration_only', 'ambient_declaration',
|
|
46
|
+
'abstract_bodyless', 'overload_signature',
|
|
47
|
+
]);
|
|
48
|
+
const sidecarKeys = [
|
|
49
|
+
'schema', 'recordCap', 'bodyEligibility', 'exposures',
|
|
50
|
+
'exposureTotal', 'shownExposureCount', 'omittedExposureCount',
|
|
51
|
+
];
|
|
52
|
+
const bodyKeys = ['eligible', 'reason'];
|
|
53
|
+
const exposureKeys = ['entry', 'modulePath', 'publicName'];
|
|
54
|
+
|
|
55
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
56
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
57
|
+
? value as Record<string, unknown> : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseRecord(value: unknown): Record<string, unknown> | undefined {
|
|
61
|
+
if (typeof value !== 'string') return undefined;
|
|
62
|
+
try {
|
|
63
|
+
return record(JSON.parse(value) as unknown);
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function exactKeys(
|
|
70
|
+
value: Record<string, unknown>,
|
|
71
|
+
expected: readonly string[],
|
|
72
|
+
): boolean {
|
|
73
|
+
const keys = Object.keys(value);
|
|
74
|
+
return keys.length === expected.length
|
|
75
|
+
&& expected.every((key) => Object.hasOwn(value, key));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function nonEmpty(value: unknown): value is string {
|
|
79
|
+
return typeof value === 'string' && value.length > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bodyEligibility(
|
|
83
|
+
value: unknown,
|
|
84
|
+
): ExecutableBodyEligibility | undefined {
|
|
85
|
+
const body = record(value);
|
|
86
|
+
if (!body || !exactKeys(body, bodyKeys)
|
|
87
|
+
|| typeof body.eligible !== 'boolean'
|
|
88
|
+
|| typeof body.reason !== 'string'
|
|
89
|
+
|| !bodyReasons.has(body.reason)
|
|
90
|
+
|| body.eligible !== (body.reason === 'body_present')) return undefined;
|
|
91
|
+
return body as unknown as ExecutableBodyEligibility;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function exposure(value: unknown): Exposure | undefined {
|
|
95
|
+
const item = record(value);
|
|
96
|
+
if (!item || !exactKeys(item, exposureKeys)
|
|
97
|
+
|| !nonEmpty(item.entry) || !nonEmpty(item.modulePath)
|
|
98
|
+
|| !nonEmpty(item.publicName)
|
|
99
|
+
|| (item.entry !== '.' && !item.entry.startsWith('./'))) return undefined;
|
|
100
|
+
return item as unknown as Exposure;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function exposureKey(value: Exposure): string {
|
|
104
|
+
return `${value.entry}\0${value.modulePath}\0${value.publicName}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parsedExposures(value: unknown): Exposure[] | undefined {
|
|
108
|
+
if (!Array.isArray(value)) return undefined;
|
|
109
|
+
const values = value.flatMap((item) => {
|
|
110
|
+
const parsed = exposure(item);
|
|
111
|
+
return parsed ? [parsed] : [];
|
|
112
|
+
});
|
|
113
|
+
const keys = new Set(values.map(exposureKey));
|
|
114
|
+
return values.length === value.length && keys.size === values.length
|
|
115
|
+
? values : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sidecar(value: unknown): SymbolSidecar | undefined {
|
|
119
|
+
const item = record(value);
|
|
120
|
+
if (!item || !exactKeys(item, sidecarKeys)
|
|
121
|
+
|| item.schema !== PACKAGE_PUBLIC_SURFACE_SCHEMA
|
|
122
|
+
|| item.recordCap !== PACKAGE_PUBLIC_SURFACE_RECORD_CAP) return undefined;
|
|
123
|
+
const body = bodyEligibility(item.bodyEligibility);
|
|
124
|
+
const exposures = parsedExposures(item.exposures);
|
|
125
|
+
if (!body || !exposures || exposures.length === 0
|
|
126
|
+
|| !sidecarCountsValid(item, exposures.length))
|
|
127
|
+
return undefined;
|
|
128
|
+
return { body, exposures };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sidecarCountsValid(
|
|
132
|
+
item: Record<string, unknown>,
|
|
133
|
+
exposureCount: number,
|
|
134
|
+
): boolean {
|
|
135
|
+
return exposureCount <= PACKAGE_PUBLIC_SURFACE_RECORD_CAP
|
|
136
|
+
&& item.exposureTotal === exposureCount
|
|
137
|
+
&& item.shownExposureCount === exposureCount
|
|
138
|
+
&& item.omittedExposureCount === 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function currentRepositories(
|
|
142
|
+
db: Db,
|
|
143
|
+
workspaceId?: number,
|
|
144
|
+
): SurfaceRepository[] {
|
|
145
|
+
const rows = db.prepare(`SELECT id,package_name packageName,
|
|
146
|
+
package_public_surface_json surfaceJson FROM repositories
|
|
147
|
+
WHERE fact_analyzer_version=?
|
|
148
|
+
AND (? IS NULL OR workspace_id=?)
|
|
149
|
+
ORDER BY id`).all(ANALYZER_VERSION, workspaceId, workspaceId);
|
|
150
|
+
return rows.flatMap((row) => {
|
|
151
|
+
if (typeof row.id !== 'number') return [];
|
|
152
|
+
const packageName = typeof row.packageName === 'string'
|
|
153
|
+
? row.packageName : null;
|
|
154
|
+
const value = parseRecord(row.surfaceJson);
|
|
155
|
+
return [{
|
|
156
|
+
id: row.id,
|
|
157
|
+
surface: value
|
|
158
|
+
? parsePackagePublicSurfaceFact(value, packageName)
|
|
159
|
+
: undefined,
|
|
160
|
+
symbols: [],
|
|
161
|
+
}];
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function currentSymbols(
|
|
166
|
+
db: Db,
|
|
167
|
+
workspaceId?: number,
|
|
168
|
+
): SurfaceSymbol[] {
|
|
169
|
+
const rows = db.prepare(`SELECT s.id,s.repo_id repoId,
|
|
170
|
+
s.source_file sourceFile,s.kind,s.qualified_name qualifiedName,
|
|
171
|
+
s.start_offset startOffset,s.end_offset endOffset,
|
|
172
|
+
s.evidence_json evidenceJson
|
|
173
|
+
FROM symbols s JOIN repositories r ON r.id=s.repo_id
|
|
174
|
+
WHERE r.fact_analyzer_version=?
|
|
175
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
176
|
+
ORDER BY s.id`).all(ANALYZER_VERSION, workspaceId, workspaceId);
|
|
177
|
+
return rows.flatMap(surfaceSymbol);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function surfaceSymbol(row: Record<string, unknown>): SurfaceSymbol[] {
|
|
181
|
+
if (typeof row.id !== 'number' || typeof row.repoId !== 'number') return [];
|
|
182
|
+
const carrierValid = validSymbolCarrier(row);
|
|
183
|
+
return [{
|
|
184
|
+
id: row.id,
|
|
185
|
+
repoId: row.repoId,
|
|
186
|
+
carrierValid,
|
|
187
|
+
sourceFile: stringField(row.sourceFile),
|
|
188
|
+
kind: stringField(row.kind),
|
|
189
|
+
qualifiedName: stringField(row.qualifiedName),
|
|
190
|
+
startOffset: numberField(row.startOffset),
|
|
191
|
+
endOffset: numberField(row.endOffset),
|
|
192
|
+
evidence: parseRecord(row.evidenceJson),
|
|
193
|
+
}];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validSymbolCarrier(row: Record<string, unknown>): boolean {
|
|
197
|
+
return typeof row.sourceFile === 'string' && typeof row.kind === 'string'
|
|
198
|
+
&& typeof row.qualifiedName === 'string'
|
|
199
|
+
&& typeof row.startOffset === 'number'
|
|
200
|
+
&& typeof row.endOffset === 'number' && row.startOffset >= 0
|
|
201
|
+
&& row.endOffset > row.startOffset;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stringField(value: unknown): string {
|
|
205
|
+
return typeof value === 'string' ? value : '';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function numberField(value: unknown): number {
|
|
209
|
+
return typeof value === 'number' ? value : -1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function repositoryAudits(
|
|
213
|
+
db: Db,
|
|
214
|
+
workspaceId?: number,
|
|
215
|
+
): SurfaceRepository[] {
|
|
216
|
+
const repositories = currentRepositories(db, workspaceId);
|
|
217
|
+
const byId = new Map(repositories.map((item) => [item.id, item]));
|
|
218
|
+
for (const symbol of currentSymbols(db, workspaceId))
|
|
219
|
+
byId.get(symbol.repoId)?.symbols.push(symbol);
|
|
220
|
+
return repositories;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function targetMatches(
|
|
224
|
+
target: PublicSurfaceTarget,
|
|
225
|
+
symbol: SurfaceSymbol,
|
|
226
|
+
): boolean {
|
|
227
|
+
return target.sourceFile === symbol.sourceFile
|
|
228
|
+
&& target.kind === symbol.kind
|
|
229
|
+
&& target.qualifiedName === symbol.qualifiedName
|
|
230
|
+
&& target.startOffset === symbol.startOffset
|
|
231
|
+
&& target.endOffset === symbol.endOffset;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function bodyMatches(
|
|
235
|
+
left: ExecutableBodyEligibility,
|
|
236
|
+
right: ExecutableBodyEligibility,
|
|
237
|
+
): boolean {
|
|
238
|
+
return left.eligible === right.eligible && left.reason === right.reason;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function scopeExposure(scope: PackagePublicScope): Exposure {
|
|
242
|
+
return {
|
|
243
|
+
entry: scope.entry,
|
|
244
|
+
modulePath: scope.modulePath,
|
|
245
|
+
publicName: scope.publicName,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function exposureProofValid(
|
|
250
|
+
repository: SurfaceRepository,
|
|
251
|
+
symbol: SurfaceSymbol,
|
|
252
|
+
proof: SymbolSidecar,
|
|
253
|
+
value: Exposure,
|
|
254
|
+
): boolean {
|
|
255
|
+
if (!repository.surface) return false;
|
|
256
|
+
const scopes = repository.surface.scopes.filter((scope) =>
|
|
257
|
+
exposureKey(scopeExposure(scope)) === exposureKey(value));
|
|
258
|
+
if (scopes.length !== 1 || !scopes[0]) return false;
|
|
259
|
+
const targets = scopes[0].targets.filter((target) =>
|
|
260
|
+
targetMatches(target, symbol)
|
|
261
|
+
&& bodyMatches(target.bodyEligibility, proof.body));
|
|
262
|
+
return targets.length === 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function symbolProof(
|
|
266
|
+
repository: SurfaceRepository,
|
|
267
|
+
symbol: SurfaceSymbol,
|
|
268
|
+
): SymbolSidecar | undefined {
|
|
269
|
+
if (!symbol.carrierValid) return undefined;
|
|
270
|
+
const value = symbol.evidence?.packagePublicSurface;
|
|
271
|
+
if (value === undefined) return undefined;
|
|
272
|
+
const proof = sidecar(value);
|
|
273
|
+
if (!proof || !proof.exposures.every((item) =>
|
|
274
|
+
exposureProofValid(repository, symbol, proof, item))) return undefined;
|
|
275
|
+
return proof;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function invalidPresentSidecarCount(
|
|
279
|
+
repository: SurfaceRepository,
|
|
280
|
+
): number {
|
|
281
|
+
return repository.symbols.filter((symbol) =>
|
|
282
|
+
symbol.evidence
|
|
283
|
+
&& Object.hasOwn(symbol.evidence, 'packagePublicSurface')
|
|
284
|
+
&& !symbolProof(repository, symbol)).length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function targetProofCount(
|
|
288
|
+
repository: SurfaceRepository,
|
|
289
|
+
scope: PackagePublicScope,
|
|
290
|
+
target: PublicSurfaceTarget,
|
|
291
|
+
): number {
|
|
292
|
+
const expected = exposureKey(scopeExposure(scope));
|
|
293
|
+
return repository.symbols.filter((symbol) => {
|
|
294
|
+
if (!targetMatches(target, symbol)) return false;
|
|
295
|
+
const proof = symbolProof(repository, symbol);
|
|
296
|
+
return Boolean(proof && bodyMatches(proof.body, target.bodyEligibility)
|
|
297
|
+
&& proof.exposures.some((item) => exposureKey(item) === expected));
|
|
298
|
+
}).length;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function missingTargetProofCount(
|
|
302
|
+
repository: SurfaceRepository,
|
|
303
|
+
): number {
|
|
304
|
+
if (!repository.surface) return 0;
|
|
305
|
+
return repository.surface.scopes.reduce((scopeTotal, scope) =>
|
|
306
|
+
scopeTotal + scope.targets.filter((target) =>
|
|
307
|
+
targetProofCount(repository, scope, target) !== 1).length, 0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function invalidPackageSymbolSurfaceCount(
|
|
311
|
+
db: Db,
|
|
312
|
+
workspaceId?: number,
|
|
313
|
+
): number {
|
|
314
|
+
return repositoryAudits(db, workspaceId).reduce(
|
|
315
|
+
(total, repository) => total
|
|
316
|
+
+ invalidPresentSidecarCount(repository)
|
|
317
|
+
+ missingTargetProofCount(repository),
|
|
318
|
+
0,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { SymbolCallFact } from '../types.js';
|
|
2
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
3
|
+
import { resolveSymbolCallTarget } from './000-call-fact-repository.js';
|
|
4
|
+
import type { Db } from './connection.js';
|
|
5
|
+
|
|
6
|
+
export interface SymbolFactCategoryCount {
|
|
7
|
+
category: string;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PersistedSymbolCall {
|
|
12
|
+
repoId: number;
|
|
13
|
+
calleeId: number | null;
|
|
14
|
+
status: string;
|
|
15
|
+
reason: string | null;
|
|
16
|
+
fact: SymbolCallFact;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PersistedSymbolRows {
|
|
20
|
+
calls: PersistedSymbolCall[];
|
|
21
|
+
malformedCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
25
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
26
|
+
? value as Record<string, unknown> : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseRecord(value: unknown): Record<string, unknown> | undefined {
|
|
30
|
+
if (typeof value !== 'string') return undefined;
|
|
31
|
+
try {
|
|
32
|
+
return record(JSON.parse(value) as unknown);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nullableNumber(value: unknown): number | null {
|
|
39
|
+
return typeof value === 'number' ? value : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function allValid(values: readonly boolean[]): boolean {
|
|
43
|
+
return values.every(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function genericRows(db: Db, workspaceId?: number): PersistedSymbolRows {
|
|
47
|
+
const rows = db.prepare(`SELECT sc.* FROM symbol_calls sc
|
|
48
|
+
JOIN repositories r ON r.id=sc.repo_id
|
|
49
|
+
WHERE r.fact_analyzer_version=?
|
|
50
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
51
|
+
AND sc.import_source IS NULL
|
|
52
|
+
AND COALESCE(json_extract(sc.evidence_json,'$.relation'),'')
|
|
53
|
+
NOT IN ('package_import','package_import_derived_member')
|
|
54
|
+
AND COALESCE(json_extract(sc.evidence_json,
|
|
55
|
+
'$.importBinding.moduleKind'),'') NOT IN ('relative','package')
|
|
56
|
+
ORDER BY sc.id`).all(ANALYZER_VERSION, workspaceId, workspaceId);
|
|
57
|
+
const parsed = rows.map(persistedSymbolCall);
|
|
58
|
+
return {
|
|
59
|
+
calls: parsed.flatMap((call) => call),
|
|
60
|
+
malformedCount: parsed.filter((call) => call.length === 0).length,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function callFieldsValid(
|
|
65
|
+
row: Record<string, unknown>,
|
|
66
|
+
evidence: Record<string, unknown>,
|
|
67
|
+
): boolean {
|
|
68
|
+
return allValid([
|
|
69
|
+
typeof row.repo_id === 'number',
|
|
70
|
+
typeof row.source_file === 'string',
|
|
71
|
+
typeof row.source_line === 'number',
|
|
72
|
+
typeof row.callee_expression === 'string',
|
|
73
|
+
typeof row.call_role === 'string',
|
|
74
|
+
typeof row.status === 'string',
|
|
75
|
+
typeof evidence.caller === 'string',
|
|
76
|
+
typeof evidence.targetName === 'string',
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function persistedSymbolCall(
|
|
81
|
+
row: Record<string, unknown>,
|
|
82
|
+
): PersistedSymbolCall[] {
|
|
83
|
+
const evidence = parseRecord(row.evidence_json);
|
|
84
|
+
if (!evidence || !callFieldsValid(row, evidence)) return [];
|
|
85
|
+
return [{
|
|
86
|
+
repoId: Number(row.repo_id),
|
|
87
|
+
calleeId: nullableNumber(row.callee_symbol_id),
|
|
88
|
+
status: String(row.status),
|
|
89
|
+
reason: typeof row.unresolved_reason === 'string'
|
|
90
|
+
? row.unresolved_reason : null,
|
|
91
|
+
fact: symbolCallFact(row, evidence),
|
|
92
|
+
}];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function symbolCallFact(
|
|
96
|
+
row: Record<string, unknown>,
|
|
97
|
+
evidence: Record<string, unknown>,
|
|
98
|
+
): SymbolCallFact {
|
|
99
|
+
return {
|
|
100
|
+
callerQualifiedName: String(evidence.caller),
|
|
101
|
+
calleeExpression: String(row.callee_expression),
|
|
102
|
+
calleeLocalName: String(evidence.targetName),
|
|
103
|
+
importSource: typeof row.import_source === 'string'
|
|
104
|
+
? row.import_source : undefined,
|
|
105
|
+
sourceFile: String(row.source_file),
|
|
106
|
+
sourceLine: Number(row.source_line),
|
|
107
|
+
callSiteStartOffset: nullableNumber(row.call_site_start_offset)
|
|
108
|
+
?? undefined,
|
|
109
|
+
callSiteEndOffset: nullableNumber(row.call_site_end_offset) ?? undefined,
|
|
110
|
+
callRole: row.call_role === 'event_subscribe_handler'
|
|
111
|
+
? 'event_subscribe_handler' : 'ordinary_call',
|
|
112
|
+
evidence,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolutionMatches(db: Db, row: PersistedSymbolCall): boolean {
|
|
117
|
+
const expected = resolveSymbolCallTarget(db, row.repoId, row.fact);
|
|
118
|
+
const evidence = row.fact.evidence;
|
|
119
|
+
return allValid([
|
|
120
|
+
row.status === expected.status,
|
|
121
|
+
row.calleeId === expected.id,
|
|
122
|
+
row.reason === expected.reason,
|
|
123
|
+
evidence.candidateStrategy === expected.strategy,
|
|
124
|
+
evidence.candidateCount === expected.candidateCount,
|
|
125
|
+
evidence.eligibleCandidateCount === expected.eligibleCandidateCount,
|
|
126
|
+
evidence.selectedCandidateCount
|
|
127
|
+
=== (expected.status === 'resolved' ? 1 : 0),
|
|
128
|
+
evidence.candidateSetComplete === expected.candidateSetComplete,
|
|
129
|
+
evidence.resolvedModulePath === expected.resolvedModulePath,
|
|
130
|
+
evidence.unresolvedReason === expected.reason,
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function invalidSymbolFactCategories(
|
|
135
|
+
db: Db,
|
|
136
|
+
workspaceId?: number,
|
|
137
|
+
): SymbolFactCategoryCount[] {
|
|
138
|
+
const rows = genericRows(db, workspaceId);
|
|
139
|
+
const invalid = rows.malformedCount + rows.calls
|
|
140
|
+
.filter((row) => !resolutionMatches(db, row)).length;
|
|
141
|
+
return invalid > 0
|
|
142
|
+
? [{ category: 'symbol_call_resolution_proof_invalid', count: invalid }]
|
|
143
|
+
: [];
|
|
144
|
+
}
|