@saptools/service-flow 0.1.66 → 0.1.68

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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-TOVX4WYH.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. package/dist/chunk-TOVX4WYH.js.map +0 -1
@@ -0,0 +1,698 @@
1
+ import type { Db } from './connection.js';
2
+ import type { CallType, RepoKind } from '../types.js';
3
+ import { ANALYZER_VERSION } from '../version.js';
4
+ import {
5
+ invalidPackageFactCategories,
6
+ } from './007-package-fact-semantics.js';
7
+ import {
8
+ invalidRelativeFactCategories,
9
+ } from './008-relative-fact-semantics.js';
10
+ import {
11
+ invalidBindingFactCategories,
12
+ } from './009-binding-fact-semantics.js';
13
+ import {
14
+ invalidSymbolFactCategories,
15
+ } from './011-symbol-call-semantics.js';
16
+
17
+ export type PackageFactPhase = 'pre_package' | 'terminal';
18
+
19
+ export interface FactSemanticCategoryCount {
20
+ category: string;
21
+ count: number;
22
+ }
23
+
24
+ const executableKindSql =
25
+ "'event_registration','callback','method','object_method','function'";
26
+ const callTypeAllowlist: Record<CallType, true> = {
27
+ remote_action: true,
28
+ remote_query: true,
29
+ remote_entity_read: true,
30
+ remote_entity_mutation: true,
31
+ remote_entity_delete: true,
32
+ remote_entity_media: true,
33
+ remote_entity_candidate: true,
34
+ local_db_query: true,
35
+ external_http: true,
36
+ async_emit: true,
37
+ async_subscribe: true,
38
+ local_service_call: true,
39
+ unknown: true,
40
+ };
41
+ const repositoryKindAllowlist: Record<RepoKind, true> = {
42
+ 'cap-service': true,
43
+ 'cap-db-model': true,
44
+ 'helper-package': true,
45
+ mixed: true,
46
+ unknown: true,
47
+ };
48
+ const symbolStrategies = [
49
+ 'same_file_exact',
50
+ 'exported_exact',
51
+ 'exact_symbol_match',
52
+ 'relative_import_class_instance_method',
53
+ 'relative_import_namespace_member',
54
+ 'relative_import_static_accessor_instance_method',
55
+ 'relative_import_path_disambiguated',
56
+ 'relative_import_exported_exact',
57
+ 'proxy_member_exported_object_map',
58
+ 'proxy_member_no_global_name_fallback',
59
+ 'package_import_pending',
60
+ 'package_import_derived_member_unsupported',
61
+ 'package_public_surface_exact',
62
+ 'package_public_surface_ambiguous',
63
+ 'package_public_surface_unresolved',
64
+ ] as const;
65
+ const symbolReasons = [
66
+ 'multiple_same_file_symbol_targets',
67
+ 'multiple_exported_symbol_targets',
68
+ 'no_local_symbol_target',
69
+ 'symbol_target_has_no_executable_body',
70
+ 'relative_import_provenance_missing',
71
+ 'relative_import_type_only',
72
+ 'relative_import_module_resolution_ambiguous',
73
+ 'relative_import_requested_module_has_no_executable_body',
74
+ 'relative_import_requested_module_has_no_target',
75
+ 'multiple_relative_class_targets_in_requested_module',
76
+ 'multiple_namespace_targets_in_requested_module',
77
+ 'multiple_accessor_targets_in_requested_module',
78
+ 'multiple_exported_targets_in_requested_module',
79
+ 'multiple_proxy_targets_in_requested_module',
80
+ 'package_resolution_pending',
81
+ 'package_derived_member_provenance_insufficient',
82
+ 'package_repository_scope_ambiguous',
83
+ 'package_repository_not_indexed',
84
+ 'package_public_surface_unsupported',
85
+ 'public_surface_evidence_incomplete',
86
+ 'package_binding_type_only',
87
+ 'package_public_scope_duplicate',
88
+ 'package_public_name_not_exposed',
89
+ 'package_public_target_ambiguous',
90
+ 'public_symbol_has_no_executable_body',
91
+ ] as const;
92
+ const incompleteReasons = [
93
+ 'relative_import_provenance_missing',
94
+ 'relative_import_module_resolution_ambiguous',
95
+ 'package_resolution_pending',
96
+ 'package_repository_scope_ambiguous',
97
+ 'package_public_surface_unsupported',
98
+ 'public_surface_evidence_incomplete',
99
+ ] as const;
100
+ const packageStrategies = [
101
+ 'package_import_pending',
102
+ 'package_public_surface_exact',
103
+ 'package_public_surface_ambiguous',
104
+ 'package_public_surface_unresolved',
105
+ ] as const;
106
+ const relativeStrategies = [
107
+ 'relative_import_class_instance_method',
108
+ 'relative_import_namespace_member',
109
+ 'relative_import_static_accessor_instance_method',
110
+ 'relative_import_path_disambiguated',
111
+ 'relative_import_exported_exact',
112
+ 'proxy_member_exported_object_map',
113
+ 'proxy_member_no_global_name_fallback',
114
+ ] as const;
115
+ const simpleStrategies = [
116
+ 'same_file_exact',
117
+ 'exported_exact',
118
+ 'exact_symbol_match',
119
+ ] as const;
120
+
121
+ function sqlTextList(values: readonly string[]): string {
122
+ return values.map((value) => `'${value}'`).join(',');
123
+ }
124
+
125
+ function count(
126
+ db: Db,
127
+ sql: string,
128
+ workspaceId?: number,
129
+ predicateCount = 1,
130
+ ): number {
131
+ const params = Array.from({ length: predicateCount }, () =>
132
+ [ANALYZER_VERSION, workspaceId, workspaceId]).flat();
133
+ const row = db.prepare(sql).get(...params);
134
+ return Number(row?.count ?? 0);
135
+ }
136
+
137
+ function category(
138
+ categoryName: string,
139
+ categoryCount: number,
140
+ ): FactSemanticCategoryCount[] {
141
+ return categoryCount > 0
142
+ ? [{ category: categoryName, count: categoryCount }]
143
+ : [];
144
+ }
145
+
146
+ function currentRepositoryPredicate(alias = 'r'): string {
147
+ return `${alias}.fact_analyzer_version=?
148
+ AND (? IS NULL OR ${alias}.workspace_id=?)`;
149
+ }
150
+
151
+ function repositoryCategories(
152
+ db: Db,
153
+ workspaceId?: number,
154
+ ): FactSemanticCategoryCount[] {
155
+ const invalid = count(db, `SELECT COUNT(*) count FROM repositories r
156
+ WHERE ${currentRepositoryPredicate()}
157
+ AND (typeof(r.kind)<>'text' OR r.kind NOT IN (
158
+ ${sqlTextList(Object.keys(repositoryKindAllowlist))}
159
+ ))`, workspaceId);
160
+ return category('repository_kind_invalid', invalid);
161
+ }
162
+
163
+ function operationCategories(
164
+ db: Db,
165
+ workspaceId?: number,
166
+ ): FactSemanticCategoryCount[] {
167
+ const invalid = count(db, `SELECT COUNT(*) count
168
+ FROM cds_operations fact
169
+ JOIN cds_services service ON service.id=fact.service_id
170
+ JOIN repositories r ON r.id=service.repo_id
171
+ LEFT JOIN cds_operations base ON base.id=fact.base_operation_id
172
+ LEFT JOIN cds_services base_service ON base_service.id=base.service_id
173
+ LEFT JOIN repositories base_repo ON base_repo.id=base_service.repo_id
174
+ WHERE ${currentRepositoryPredicate()}
175
+ AND (typeof(fact.operation_type)<>'text'
176
+ OR fact.operation_type NOT IN ('action','function','event')
177
+ OR typeof(fact.operation_name)<>'text'
178
+ OR length(fact.operation_name)=0
179
+ OR typeof(fact.operation_path)<>'text'
180
+ OR length(fact.operation_path)=0
181
+ OR typeof(fact.source_file)<>'text'
182
+ OR length(fact.source_file)=0
183
+ OR typeof(fact.source_line)<>'integer' OR fact.source_line<1
184
+ OR fact.provenance NOT IN ('direct','inherited')
185
+ OR (fact.provenance='direct' AND fact.base_operation_id IS NOT NULL)
186
+ OR (fact.provenance='inherited' AND (
187
+ base.id IS NULL OR COALESCE(service.is_extend,0)<>1
188
+ OR COALESCE(service.extension_base_status,'')<>'resolved'
189
+ OR service.extension_base_service_id IS NULL
190
+ OR service.extension_base_service_id<>base.service_id
191
+ OR COALESCE(base.provenance,'')<>'direct'
192
+ OR base.base_operation_id IS NOT NULL
193
+ OR base_repo.workspace_id IS NULL
194
+ OR base_repo.workspace_id<>r.workspace_id
195
+ OR fact.operation_type<>base.operation_type
196
+ OR fact.operation_name<>base.operation_name
197
+ OR fact.operation_path<>base.operation_path
198
+ OR fact.params_json<>base.params_json
199
+ OR fact.return_type IS NOT base.return_type
200
+ OR fact.source_file<>base.source_file
201
+ OR fact.source_line<>base.source_line)))`, workspaceId);
202
+ return category('cds_operation_semantics_invalid', invalid);
203
+ }
204
+
205
+ function callSpanCategories(
206
+ db: Db,
207
+ workspaceId?: number,
208
+ ): FactSemanticCategoryCount[] {
209
+ const outbound = count(db, `SELECT COUNT(*) count
210
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
211
+ WHERE ${currentRepositoryPredicate()}
212
+ AND (typeof(fact.source_file)<>'text'
213
+ OR length(fact.source_file)=0
214
+ OR typeof(fact.call_site_start_offset)<>'integer'
215
+ OR typeof(fact.call_site_end_offset)<>'integer'
216
+ OR fact.call_site_start_offset<0
217
+ OR fact.call_site_end_offset<=fact.call_site_start_offset)`, workspaceId);
218
+ const callType = count(db, `SELECT COUNT(*) count
219
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
220
+ WHERE ${currentRepositoryPredicate()}
221
+ AND (typeof(fact.call_type)<>'text'
222
+ OR fact.call_type NOT IN (
223
+ ${sqlTextList(Object.keys(callTypeAllowlist))}
224
+ ))`, workspaceId);
225
+ const symbol = count(db, `SELECT COUNT(*) count
226
+ FROM symbol_calls fact JOIN repositories r ON r.id=fact.repo_id
227
+ WHERE ${currentRepositoryPredicate()}
228
+ AND (typeof(fact.source_file)<>'text'
229
+ OR length(fact.source_file)=0
230
+ OR typeof(fact.callee_expression)<>'text'
231
+ OR length(fact.callee_expression)=0
232
+ OR typeof(fact.call_site_start_offset)<>'integer'
233
+ OR typeof(fact.call_site_end_offset)<>'integer'
234
+ OR fact.call_site_start_offset<0
235
+ OR fact.call_site_end_offset<=fact.call_site_start_offset
236
+ OR fact.call_role NOT IN ('ordinary_call','event_subscribe_handler'))`, workspaceId);
237
+ return [
238
+ ...category('outbound_call_span_invalid', outbound),
239
+ ...category('outbound_call_type_invalid', callType),
240
+ ...category('symbol_call_span_or_role_invalid', symbol),
241
+ ];
242
+ }
243
+
244
+ function duplicateSymbolCallCategories(
245
+ db: Db,
246
+ workspaceId?: number,
247
+ ): FactSemanticCategoryCount[] {
248
+ const duplicate = count(db, `SELECT COUNT(*) count FROM (
249
+ SELECT fact.repo_id,fact.source_file,fact.call_site_start_offset,
250
+ fact.call_site_end_offset,fact.call_role,COUNT(*) duplicate_count
251
+ FROM symbol_calls fact JOIN repositories r ON r.id=fact.repo_id
252
+ WHERE ${currentRepositoryPredicate()}
253
+ GROUP BY fact.repo_id,fact.source_file,fact.call_site_start_offset,
254
+ fact.call_site_end_offset,fact.call_role
255
+ HAVING COUNT(*)<>1
256
+ )`, workspaceId);
257
+ return category('symbol_call_site_duplicate', duplicate);
258
+ }
259
+
260
+ function eventNameCategories(
261
+ db: Db,
262
+ workspaceId?: number,
263
+ ): FactSemanticCategoryCount[] {
264
+ const invalid = count(db, `SELECT COUNT(*) count
265
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
266
+ WHERE ${currentRepositoryPredicate()}
267
+ AND fact.call_type IN ('async_emit','async_subscribe')
268
+ AND (typeof(fact.event_name_expr)<>'text'
269
+ OR length(fact.event_name_expr)=0)`, workspaceId);
270
+ const duplicate = count(db, `SELECT COUNT(*) count FROM (
271
+ SELECT fact.repo_id,fact.source_file,fact.call_site_start_offset,
272
+ fact.call_site_end_offset,COUNT(*) duplicate_count
273
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
274
+ WHERE ${currentRepositoryPredicate()}
275
+ AND fact.call_type='async_subscribe'
276
+ GROUP BY fact.repo_id,fact.source_file,fact.call_site_start_offset,
277
+ fact.call_site_end_offset HAVING COUNT(*)<>1
278
+ )`, workspaceId);
279
+ return [
280
+ ...category('event_name_invalid', invalid),
281
+ ...category('async_subscription_site_duplicate', duplicate),
282
+ ];
283
+ }
284
+
285
+ function outboundOwnerCount(db: Db, workspaceId?: number): number {
286
+ return count(db, `WITH eligible AS (
287
+ SELECT fact.id fact_id,s.id symbol_id,
288
+ DENSE_RANK() OVER (PARTITION BY fact.id ORDER BY
289
+ CASE WHEN fact.call_type='async_subscribe'
290
+ AND s.kind='event_registration'
291
+ AND s.start_offset=fact.call_site_start_offset
292
+ AND s.end_offset=fact.call_site_end_offset THEN 0 ELSE 1 END,
293
+ s.end_offset-s.start_offset,
294
+ CASE s.kind WHEN 'event_registration' THEN 0 WHEN 'callback' THEN 1
295
+ WHEN 'method' THEN 2 WHEN 'object_method' THEN 3 ELSE 4 END,
296
+ s.start_offset,s.end_offset,s.qualified_name COLLATE BINARY) owner_rank
297
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
298
+ JOIN symbols s ON s.repo_id=fact.repo_id
299
+ AND s.source_file=fact.source_file
300
+ AND s.kind IN (${executableKindSql})
301
+ AND s.start_offset<=fact.call_site_start_offset
302
+ AND s.end_offset>=fact.call_site_end_offset
303
+ AND (fact.call_type<>'async_subscribe'
304
+ OR (s.kind='event_registration'
305
+ AND s.start_offset=fact.call_site_start_offset
306
+ AND s.end_offset=fact.call_site_end_offset))
307
+ WHERE ${currentRepositoryPredicate()}
308
+ ), best AS (
309
+ SELECT fact_id,COUNT(*) best_count,MAX(symbol_id) symbol_id
310
+ FROM eligible WHERE owner_rank=1 GROUP BY fact_id
311
+ )
312
+ SELECT COUNT(*) count FROM outbound_calls fact
313
+ JOIN repositories r ON r.id=fact.repo_id
314
+ LEFT JOIN best ON best.fact_id=fact.id
315
+ WHERE ${currentRepositoryPredicate()}
316
+ AND ((best.fact_id IS NULL AND (
317
+ fact.source_symbol_id IS NOT NULL
318
+ OR COALESCE(json_extract(fact.evidence_json,
319
+ '$.sourceOwnerResolution'),'')
320
+ <>'ownerless_file_scope'))
321
+ OR (best.fact_id IS NOT NULL AND (
322
+ best.best_count<>1 OR fact.source_symbol_id IS NOT best.symbol_id
323
+ OR COALESCE(json_extract(fact.evidence_json,
324
+ '$.sourceOwnerResolution'),'')<>'owned_exact')))`, workspaceId, 2);
325
+ }
326
+
327
+ function symbolOwnerCount(db: Db, workspaceId?: number): number {
328
+ return count(db, `WITH eligible AS (
329
+ SELECT fact.id fact_id,s.id symbol_id,
330
+ DENSE_RANK() OVER (PARTITION BY fact.id ORDER BY
331
+ CASE WHEN fact.call_role='event_subscribe_handler'
332
+ AND s.kind='event_registration'
333
+ AND s.start_offset=fact.call_site_start_offset
334
+ AND s.end_offset=fact.call_site_end_offset THEN 0 ELSE 1 END,
335
+ s.end_offset-s.start_offset,
336
+ CASE s.kind WHEN 'event_registration' THEN 0 WHEN 'callback' THEN 1
337
+ WHEN 'method' THEN 2 WHEN 'object_method' THEN 3 ELSE 4 END,
338
+ s.start_offset,s.end_offset,s.qualified_name COLLATE BINARY) owner_rank
339
+ FROM symbol_calls fact JOIN repositories r ON r.id=fact.repo_id
340
+ JOIN symbols s ON s.repo_id=fact.repo_id
341
+ AND s.source_file=fact.source_file
342
+ AND s.kind IN (${executableKindSql})
343
+ AND s.start_offset<=fact.call_site_start_offset
344
+ AND s.end_offset>=fact.call_site_end_offset
345
+ AND (fact.call_role<>'event_subscribe_handler'
346
+ OR (s.kind='event_registration'
347
+ AND s.start_offset=fact.call_site_start_offset
348
+ AND s.end_offset=fact.call_site_end_offset))
349
+ WHERE ${currentRepositoryPredicate()}
350
+ ), best AS (
351
+ SELECT fact_id,COUNT(*) best_count,MAX(symbol_id) symbol_id
352
+ FROM eligible WHERE owner_rank=1 GROUP BY fact_id
353
+ )
354
+ SELECT COUNT(*) count FROM symbol_calls fact
355
+ JOIN repositories r ON r.id=fact.repo_id
356
+ LEFT JOIN best ON best.fact_id=fact.id
357
+ LEFT JOIN symbols selected ON selected.id=best.symbol_id
358
+ WHERE ${currentRepositoryPredicate()}
359
+ AND (best.fact_id IS NULL OR best.best_count<>1
360
+ OR fact.caller_symbol_id IS NOT best.symbol_id
361
+ OR typeof(json_extract(fact.evidence_json,'$.caller'))<>'text'
362
+ OR json_extract(fact.evidence_json,'$.caller') COLLATE BINARY
363
+ <>selected.qualified_name COLLATE BINARY)`, workspaceId, 2);
364
+ }
365
+
366
+ function ownerCategories(
367
+ db: Db,
368
+ workspaceId?: number,
369
+ ): FactSemanticCategoryCount[] {
370
+ return [
371
+ ...category('outbound_call_owner_invalid',
372
+ outboundOwnerCount(db, workspaceId)),
373
+ ...category('symbol_call_owner_invalid',
374
+ symbolOwnerCount(db, workspaceId)),
375
+ ];
376
+ }
377
+
378
+ function bindingSiteCount(db: Db, workspaceId?: number): number {
379
+ return count(db, `SELECT COUNT(*) count
380
+ FROM service_bindings fact JOIN repositories r ON r.id=fact.repo_id
381
+ LEFT JOIN symbols owner ON owner.id=fact.symbol_id
382
+ WHERE ${currentRepositoryPredicate()}
383
+ AND (typeof(fact.binding_site_start_offset)<>'integer'
384
+ OR typeof(fact.binding_site_end_offset)<>'integer'
385
+ OR typeof(fact.source_file)<>'text'
386
+ OR length(fact.source_file)=0
387
+ OR typeof(fact.variable_name)<>'text'
388
+ OR length(fact.variable_name)=0
389
+ OR fact.binding_site_start_offset<0
390
+ OR fact.binding_site_end_offset<=fact.binding_site_start_offset
391
+ OR fact.owner_resolution NOT IN
392
+ ('owned_exact','ownerless_file_scope')
393
+ OR (fact.owner_resolution='owned_exact' AND (
394
+ owner.id IS NULL OR owner.repo_id<>fact.repo_id
395
+ OR owner.source_file<>fact.source_file
396
+ OR owner.kind NOT IN (${executableKindSql})
397
+ OR owner.start_offset>fact.binding_site_start_offset
398
+ OR owner.end_offset<fact.binding_site_end_offset))
399
+ OR (fact.owner_resolution='ownerless_file_scope'
400
+ AND fact.symbol_id IS NOT NULL))`, workspaceId);
401
+ }
402
+
403
+ function duplicateBindingSiteCount(db: Db, workspaceId?: number): number {
404
+ return count(db, `SELECT COUNT(*) count FROM (
405
+ SELECT fact.repo_id,fact.source_file,fact.variable_name,
406
+ fact.binding_site_start_offset,fact.binding_site_end_offset,COUNT(*) total
407
+ FROM service_bindings fact JOIN repositories r ON r.id=fact.repo_id
408
+ WHERE ${currentRepositoryPredicate()}
409
+ GROUP BY fact.repo_id,fact.source_file,fact.variable_name,
410
+ fact.binding_site_start_offset,fact.binding_site_end_offset
411
+ HAVING COUNT(*)<>1
412
+ )`, workspaceId);
413
+ }
414
+
415
+ function bindingCategories(
416
+ db: Db,
417
+ workspaceId?: number,
418
+ ): FactSemanticCategoryCount[] {
419
+ return [
420
+ ...category('service_binding_site_or_owner_invalid',
421
+ bindingSiteCount(db, workspaceId)),
422
+ ...category('service_binding_exact_site_duplicate',
423
+ duplicateBindingSiteCount(db, workspaceId)),
424
+ ...category('outbound_binding_reference_invalid',
425
+ bindingReferenceCount(db, workspaceId)),
426
+ ];
427
+ }
428
+
429
+ function bindingReferenceCount(db: Db, workspaceId?: number): number {
430
+ return count(db, `SELECT COUNT(*) count
431
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
432
+ LEFT JOIN service_bindings binding ON binding.id=fact.service_binding_id
433
+ WHERE ${currentRepositoryPredicate()}
434
+ AND (
435
+ COALESCE(json_extract(fact.evidence_json,
436
+ '$.serviceBindingReference.status'),'')
437
+ NOT IN ('resolved_exact','ambiguous','unresolved','not_applicable')
438
+ OR (json_extract(fact.evidence_json,
439
+ '$.serviceBindingReference.status')='resolved_exact' AND (
440
+ binding.id IS NULL OR binding.repo_id<>fact.repo_id
441
+ OR binding.source_file IS NOT json_extract(fact.evidence_json,
442
+ '$.serviceBindingReference.bindingSourceFile')
443
+ OR binding.variable_name IS NOT json_extract(fact.evidence_json,
444
+ '$.serviceBindingReference.variableName')
445
+ OR binding.binding_site_start_offset IS NOT json_extract(
446
+ fact.evidence_json,
447
+ '$.serviceBindingReference.bindingSiteStartOffset')
448
+ OR binding.binding_site_end_offset IS NOT json_extract(
449
+ fact.evidence_json,
450
+ '$.serviceBindingReference.bindingSiteEndOffset')))
451
+ OR (json_extract(fact.evidence_json,
452
+ '$.serviceBindingReference.status') IN ('ambiguous','unresolved')
453
+ AND (fact.service_binding_id IS NOT NULL
454
+ OR typeof(json_extract(fact.evidence_json,
455
+ '$.serviceBindingReference.reason'))<>'text'))
456
+ OR (json_extract(fact.evidence_json,
457
+ '$.serviceBindingReference.status')='not_applicable'
458
+ AND (fact.service_binding_id IS NOT NULL
459
+ OR json_extract(fact.evidence_json,
460
+ '$.serviceBindingReference.variableName') IS NOT NULL))
461
+ )`, workspaceId);
462
+ }
463
+
464
+ function handlerStatusCount(db: Db, workspaceId?: number): number {
465
+ return count(db, `SELECT COUNT(*) count FROM outbound_calls fact
466
+ JOIN repositories r ON r.id=fact.repo_id
467
+ WHERE ${currentRepositoryPredicate()}
468
+ AND fact.call_type='async_subscribe'
469
+ AND (
470
+ COALESCE(json_extract(fact.evidence_json,
471
+ '$.handlerReferenceStatus'),'')
472
+ NOT IN ('role_required','unsupported_inline','unsupported_wrapper',
473
+ 'unsupported_reference_shape','missing_argument')
474
+ OR (json_extract(fact.evidence_json,
475
+ '$.handlerReferenceStatus')='role_required' AND (
476
+ json_extract(fact.evidence_json,
477
+ '$.handlerReferenceReason') IS NOT NULL
478
+ OR COALESCE(json_extract(fact.evidence_json,
479
+ '$.handlerReferenceShape'),'') NOT IN (
480
+ 'identifier','namespace_member','static_member','default_member',
481
+ 'wrapped_identifier','wrapped_namespace_member',
482
+ 'wrapped_static_member','wrapped_default_member')))
483
+ OR (json_extract(fact.evidence_json,
484
+ '$.handlerReferenceStatus')='unsupported_inline' AND (
485
+ COALESCE(json_extract(fact.evidence_json,
486
+ '$.handlerReferenceReason'),'')<>'inline_handler_body_not_indexed'
487
+ OR COALESCE(json_extract(fact.evidence_json,
488
+ '$.handlerReferenceShape'),'')<>'inline_callback'))
489
+ OR (json_extract(fact.evidence_json,
490
+ '$.handlerReferenceStatus')='unsupported_wrapper' AND (
491
+ COALESCE(json_extract(fact.evidence_json,
492
+ '$.handlerReferenceReason'),'') NOT IN (
493
+ 'wrapper_requires_one_reference',
494
+ 'wrapper_reference_shape_unsupported')
495
+ OR COALESCE(json_extract(fact.evidence_json,
496
+ '$.handlerReferenceShape'),'')<>'wrapper_call'))
497
+ OR (json_extract(fact.evidence_json,
498
+ '$.handlerReferenceStatus')='unsupported_reference_shape' AND (
499
+ COALESCE(json_extract(fact.evidence_json,
500
+ '$.handlerReferenceReason'),'')
501
+ <>'handler_reference_shape_unsupported'
502
+ OR COALESCE(json_extract(fact.evidence_json,
503
+ '$.handlerReferenceShape'),'')<>'unsupported_expression'))
504
+ OR (json_extract(fact.evidence_json,
505
+ '$.handlerReferenceStatus')='missing_argument' AND (
506
+ COALESCE(json_extract(fact.evidence_json,
507
+ '$.handlerReferenceReason'),'')<>'handler_argument_missing'
508
+ OR COALESCE(json_extract(fact.evidence_json,
509
+ '$.handlerReferenceShape'),'')<>'missing'))
510
+ )`, workspaceId);
511
+ }
512
+
513
+ function handlerAssociationCount(db: Db, workspaceId?: number): number {
514
+ return count(db, `SELECT COUNT(*) count FROM (
515
+ SELECT fact.id,
516
+ json_extract(fact.evidence_json,
517
+ '$.handlerReferenceStatus') handler_status,
518
+ COUNT(role.id) role_count
519
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
520
+ LEFT JOIN symbol_calls role ON role.repo_id=fact.repo_id
521
+ AND role.source_file=fact.source_file
522
+ AND role.call_site_start_offset=fact.call_site_start_offset
523
+ AND role.call_site_end_offset=fact.call_site_end_offset
524
+ AND role.call_role='event_subscribe_handler'
525
+ WHERE ${currentRepositoryPredicate()}
526
+ AND fact.call_type='async_subscribe'
527
+ GROUP BY fact.id HAVING
528
+ (handler_status='role_required' AND role_count<>1)
529
+ OR (handler_status<>'role_required' AND role_count<>0)
530
+ )`, workspaceId);
531
+ }
532
+
533
+ function handlerRoleCount(db: Db, workspaceId?: number): number {
534
+ return count(db, `SELECT COUNT(*) count FROM symbol_calls fact
535
+ JOIN repositories r ON r.id=fact.repo_id
536
+ LEFT JOIN outbound_calls subscription
537
+ ON subscription.repo_id=fact.repo_id
538
+ AND subscription.source_file=fact.source_file
539
+ AND subscription.call_site_start_offset=fact.call_site_start_offset
540
+ AND subscription.call_site_end_offset=fact.call_site_end_offset
541
+ AND subscription.call_type='async_subscribe'
542
+ WHERE ${currentRepositoryPredicate()}
543
+ AND fact.call_role='event_subscribe_handler'
544
+ AND (subscription.id IS NULL
545
+ OR subscription.source_line<>fact.source_line
546
+ OR subscription.source_symbol_id IS NOT fact.caller_symbol_id
547
+ OR COALESCE(json_extract(fact.evidence_json,'$.factOrigin'),'')
548
+ <>'event_subscribe_handler_reference'
549
+ OR typeof(json_extract(fact.evidence_json,'$.candidateStrategy'))
550
+ <>'text'
551
+ OR length(json_extract(fact.evidence_json,'$.candidateStrategy'))=0)`,
552
+ workspaceId);
553
+ }
554
+
555
+ function eventAssociationCategories(
556
+ db: Db,
557
+ workspaceId?: number,
558
+ ): FactSemanticCategoryCount[] {
559
+ return [
560
+ ...category('subscription_handler_status_invalid',
561
+ handlerStatusCount(db, workspaceId)),
562
+ ...category('subscription_handler_cardinality_invalid',
563
+ handlerAssociationCount(db, workspaceId)),
564
+ ...category('event_handler_role_provenance_invalid',
565
+ handlerRoleCount(db, workspaceId)),
566
+ ];
567
+ }
568
+
569
+ const symbolResolutionSql = `SELECT COUNT(*) count FROM symbol_calls fact
570
+ JOIN repositories r ON r.id=fact.repo_id
571
+ LEFT JOIN symbols target ON target.id=fact.callee_symbol_id
572
+ LEFT JOIN repositories target_repo ON target_repo.id=target.repo_id
573
+ WHERE ${currentRepositoryPredicate()}
574
+ AND (fact.status NOT IN ('resolved','ambiguous','unresolved')
575
+ OR typeof(json_extract(fact.evidence_json,'$.candidateStrategy'))
576
+ <>'text'
577
+ OR length(json_extract(fact.evidence_json,'$.candidateStrategy'))=0
578
+ OR json_extract(fact.evidence_json,'$.candidateStrategy')
579
+ NOT IN (${sqlTextList(symbolStrategies)})
580
+ OR typeof(json_extract(fact.evidence_json,'$.candidateCount'))
581
+ <>'integer'
582
+ OR typeof(json_extract(fact.evidence_json,'$.eligibleCandidateCount'))
583
+ <>'integer'
584
+ OR typeof(json_extract(fact.evidence_json,'$.selectedCandidateCount'))
585
+ <>'integer'
586
+ OR COALESCE(json_type(
587
+ fact.evidence_json,'$.candidateSetComplete'),'missing')
588
+ NOT IN ('true','false')
589
+ OR json_extract(fact.evidence_json,'$.candidateCount')<0
590
+ OR json_extract(fact.evidence_json,'$.eligibleCandidateCount')<0
591
+ OR json_extract(fact.evidence_json,'$.selectedCandidateCount')
592
+ NOT IN (0,1)
593
+ OR json_extract(fact.evidence_json,'$.selectedCandidateCount')
594
+ >json_extract(fact.evidence_json,'$.eligibleCandidateCount')
595
+ OR json_extract(fact.evidence_json,'$.eligibleCandidateCount')
596
+ >json_extract(fact.evidence_json,'$.candidateCount')
597
+ OR fact.unresolved_reason IS NOT json_extract(
598
+ fact.evidence_json,'$.unresolvedReason')
599
+ OR (fact.status='resolved' AND (
600
+ fact.callee_symbol_id IS NULL OR target.id IS NULL
601
+ OR target_repo.workspace_id IS NOT r.workspace_id
602
+ OR json_extract(fact.evidence_json,'$.candidateSetComplete')<>1
603
+ OR json_extract(fact.evidence_json,'$.eligibleCandidateCount')<>1
604
+ OR json_extract(fact.evidence_json,'$.selectedCandidateCount')<>1
605
+ OR fact.unresolved_reason IS NOT NULL
606
+ OR COALESCE(json_type(
607
+ fact.evidence_json,'$.unresolvedReason'),'missing')<>'null'))
608
+ OR (fact.status='ambiguous' AND (
609
+ fact.callee_symbol_id IS NOT NULL
610
+ OR json_extract(fact.evidence_json,'$.candidateSetComplete')<>1
611
+ OR json_extract(fact.evidence_json,'$.eligibleCandidateCount')<2
612
+ OR json_extract(fact.evidence_json,'$.selectedCandidateCount')<>0
613
+ OR typeof(fact.unresolved_reason)<>'text'
614
+ OR length(fact.unresolved_reason)=0
615
+ OR COALESCE(json_type(
616
+ fact.evidence_json,'$.unresolvedReason'),'missing')<>'text'
617
+ OR fact.unresolved_reason NOT IN (${sqlTextList(symbolReasons)})))
618
+ OR (fact.status='unresolved' AND (
619
+ fact.callee_symbol_id IS NOT NULL
620
+ OR json_extract(fact.evidence_json,'$.selectedCandidateCount')<>0
621
+ OR typeof(fact.unresolved_reason)<>'text'
622
+ OR length(fact.unresolved_reason)=0
623
+ OR COALESCE(json_type(
624
+ fact.evidence_json,'$.unresolvedReason'),'missing')<>'text'
625
+ OR fact.unresolved_reason NOT IN (${sqlTextList(symbolReasons)})
626
+ OR (json_extract(fact.evidence_json,'$.candidateSetComplete')=1
627
+ AND json_extract(fact.evidence_json,
628
+ '$.eligibleCandidateCount')<>0)
629
+ OR (json_extract(fact.evidence_json,'$.candidateSetComplete')=0
630
+ AND fact.unresolved_reason NOT IN (
631
+ ${sqlTextList(incompleteReasons)}))))
632
+ OR (json_extract(fact.evidence_json,'$.candidateStrategy')
633
+ ='package_import_pending'
634
+ AND (COALESCE(json_extract(fact.evidence_json,'$.relation'),'')
635
+ <>'package_import'
636
+ OR COALESCE(json_extract(fact.evidence_json,
637
+ '$.importBinding.moduleKind'),'')<>'package'))
638
+ OR (json_extract(fact.evidence_json,'$.candidateStrategy')
639
+ IN (${sqlTextList(packageStrategies)})
640
+ AND (COALESCE(json_extract(
641
+ fact.evidence_json,'$.relation'),'')<>'package_import'
642
+ OR COALESCE(json_extract(fact.evidence_json,
643
+ '$.importBinding.moduleKind'),'')<>'package'))
644
+ OR (json_extract(fact.evidence_json,'$.candidateStrategy')
645
+ ='package_import_derived_member_unsupported'
646
+ AND (COALESCE(json_extract(fact.evidence_json,'$.relation'),'')
647
+ <>'package_import_derived_member'
648
+ OR COALESCE(json_extract(fact.evidence_json,
649
+ '$.derivedImportBinding.moduleKind'),'')<>'package'))
650
+ OR (json_extract(fact.evidence_json,'$.candidateStrategy')
651
+ IN (${sqlTextList(relativeStrategies)})
652
+ AND (COALESCE(json_extract(fact.evidence_json,
653
+ '$.importBinding.moduleKind'),'')<>'relative'
654
+ OR COALESCE(fact.import_source,'') NOT LIKE '.%'))
655
+ OR (json_extract(fact.evidence_json,'$.candidateStrategy')
656
+ IN (${sqlTextList(simpleStrategies)})
657
+ AND (fact.import_source IS NOT NULL
658
+ OR COALESCE(json_extract(fact.evidence_json,
659
+ '$.importBinding.moduleKind'),'') IN ('relative','package')
660
+ OR COALESCE(json_extract(fact.evidence_json,'$.relation'),'')
661
+ IN ('package_import','package_import_derived_member')))
662
+ __PACKAGE_PENDING_TERMINAL_CHECK__)`;
663
+
664
+ function symbolResolutionCount(
665
+ db: Db,
666
+ workspaceId: number | undefined,
667
+ phase: PackageFactPhase,
668
+ ): number {
669
+ const pendingInvalid = phase === 'terminal'
670
+ ? "OR json_extract(fact.evidence_json,'$.candidateStrategy')='package_import_pending'"
671
+ : '';
672
+ return count(db, symbolResolutionSql.replace(
673
+ '__PACKAGE_PENDING_TERMINAL_CHECK__', pendingInvalid,
674
+ ), workspaceId);
675
+ }
676
+
677
+ export function invalidFactSemanticCategories(
678
+ db: Db,
679
+ workspaceId?: number,
680
+ phase: PackageFactPhase = 'pre_package',
681
+ ): FactSemanticCategoryCount[] {
682
+ return [
683
+ ...repositoryCategories(db, workspaceId),
684
+ ...operationCategories(db, workspaceId),
685
+ ...callSpanCategories(db, workspaceId),
686
+ ...duplicateSymbolCallCategories(db, workspaceId),
687
+ ...eventNameCategories(db, workspaceId),
688
+ ...ownerCategories(db, workspaceId),
689
+ ...bindingCategories(db, workspaceId),
690
+ ...invalidBindingFactCategories(db, workspaceId),
691
+ ...eventAssociationCategories(db, workspaceId),
692
+ ...invalidPackageFactCategories(db, workspaceId, phase),
693
+ ...invalidRelativeFactCategories(db, workspaceId),
694
+ ...invalidSymbolFactCategories(db, workspaceId),
695
+ ...category('symbol_call_resolution_matrix_invalid',
696
+ symbolResolutionCount(db, workspaceId, phase)),
697
+ ];
698
+ }