@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +27 -9
  3. package/TECHNICAL-NOTE.md +36 -0
  4. package/dist/chunk-3N3B5KHV.js +19596 -0
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +2645 -521
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +67 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli/003-doctor-package-resolution.ts +68 -0
  13. package/src/cli/doctor.ts +25 -14
  14. package/src/cli.ts +151 -87
  15. package/src/db/000-call-fact-repository.ts +499 -340
  16. package/src/db/001-fact-lifecycle.ts +60 -30
  17. package/src/db/002-fact-json-inventory.ts +169 -0
  18. package/src/db/003-current-fact-semantics.ts +699 -0
  19. package/src/db/004-package-target-invalidation.ts +183 -0
  20. package/src/db/005-schema-structure.ts +201 -0
  21. package/src/db/006-relative-symbol-resolution.ts +464 -0
  22. package/src/db/007-package-fact-semantics.ts +573 -0
  23. package/src/db/008-relative-fact-semantics.ts +210 -0
  24. package/src/db/009-binding-fact-semantics.ts +352 -0
  25. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  26. package/src/db/011-symbol-call-semantics.ts +144 -0
  27. package/src/db/012-binding-reference-proof.ts +268 -0
  28. package/src/db/013-index-publication-failure.ts +91 -0
  29. package/src/db/014-binding-helper-provenance.ts +17 -0
  30. package/src/db/migrations.ts +16 -3
  31. package/src/db/repositories.ts +130 -6
  32. package/src/db/schema.ts +4 -2
  33. package/src/index.ts +12 -0
  34. package/src/indexer/cds-extension-resolver.ts +27 -3
  35. package/src/indexer/repository-indexer.ts +135 -13
  36. package/src/indexer/workspace-indexer.ts +237 -34
  37. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  38. package/src/linker/004-event-subscription-handler-linker.ts +34 -11
  39. package/src/linker/005-odata-path-structure.ts +371 -0
  40. package/src/linker/006-event-template-link.ts +72 -0
  41. package/src/linker/007-call-edge-insertion.ts +568 -0
  42. package/src/linker/cross-repo-linker.ts +4 -166
  43. package/src/linker/odata-path-normalizer.ts +273 -180
  44. package/src/linker/service-resolver.ts +197 -77
  45. package/src/parsers/000-direct-query-execution.ts +11 -0
  46. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  47. package/src/parsers/003-package-public-surface.ts +661 -0
  48. package/src/parsers/004-fact-identity.ts +108 -0
  49. package/src/parsers/005-event-subscription-facts.ts +281 -0
  50. package/src/parsers/006-binding-identity.ts +348 -0
  51. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  52. package/src/parsers/008-package-surface-publication.ts +82 -0
  53. package/src/parsers/009-symbol-call-facts.ts +528 -0
  54. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  55. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  56. package/src/parsers/012-package-fact-contract.ts +306 -0
  57. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  58. package/src/parsers/014-service-binding-helper-flow.ts +306 -0
  59. package/src/parsers/015-service-binding-collector.ts +693 -0
  60. package/src/parsers/016-local-symbol-reference.ts +261 -0
  61. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  62. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  63. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  64. package/src/parsers/020-stable-local-value.ts +217 -0
  65. package/src/parsers/021-binding-visibility.ts +168 -0
  66. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  67. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  68. package/src/parsers/operation-path-analysis.ts +6 -1
  69. package/src/parsers/outbound-call-parser.ts +162 -512
  70. package/src/parsers/package-json-parser.ts +45 -3
  71. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  72. package/src/parsers/service-binding-parser.ts +147 -597
  73. package/src/parsers/symbol-parser.ts +513 -352
  74. package/src/trace/002-trace-diagnostics.ts +36 -1
  75. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  76. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  77. package/src/trace/013-trace-root-scopes.ts +2 -3
  78. package/src/trace/014-compact-contract.ts +6 -0
  79. package/src/trace/015-trace-edge-recorder.ts +61 -4
  80. package/src/trace/016-compact-projector.ts +15 -17
  81. package/src/trace/019-trace-edge-semantics.ts +6 -10
  82. package/src/trace/020-compact-field-projection.ts +122 -38
  83. package/src/trace/021-compact-decision-normalization.ts +171 -0
  84. package/src/trace/022-trace-fact-preflight.ts +21 -0
  85. package/src/trace/023-nested-event-scopes.ts +23 -0
  86. package/src/trace/024-compact-observation-decision.ts +81 -0
  87. package/src/trace/025-trace-implementation-scope.ts +123 -0
  88. package/src/trace/026-trace-start-scope.ts +336 -0
  89. package/src/trace/027-trace-scope-execution.ts +566 -0
  90. package/src/trace/028-trace-operation-execution.ts +336 -0
  91. package/src/trace/029-trace-start-implementation.ts +172 -0
  92. package/src/trace/030-event-runtime-resolution.ts +151 -0
  93. package/src/trace/031-local-call-expansion.ts +37 -0
  94. package/src/trace/implementation-hints.ts +9 -6
  95. package/src/trace/selectors.ts +1 -0
  96. package/src/trace/trace-engine.ts +122 -624
  97. package/src/types.ts +57 -0
  98. package/src/utils/001-placeholders.ts +188 -10
  99. package/src/version.ts +1 -1
  100. package/dist/chunk-ZQABU7MR.js +0 -12151
  101. 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
+ }