@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,210 @@
1
+ import type { Db } from './connection.js';
2
+ import { ANALYZER_VERSION } from '../version.js';
3
+ import {
4
+ resolveRelativeSymbolCall,
5
+ } from './006-relative-symbol-resolution.js';
6
+ import type { SymbolCallFact } from '../types.js';
7
+ import { parseRelativeImportReference } from
8
+ '../parsers/012-package-fact-contract.js';
9
+ import type { SymbolImportReference } from
10
+ '../parsers/002-symbol-import-bindings.js';
11
+
12
+ export interface RelativeFactCategoryCount {
13
+ category: string;
14
+ count: number;
15
+ }
16
+
17
+ interface PersistedRelativeCall {
18
+ repoId: number;
19
+ calleeId: number | null;
20
+ status: string;
21
+ reason: string | null;
22
+ fact: SymbolCallFact;
23
+ }
24
+
25
+ interface PersistedRelativeRows {
26
+ calls: PersistedRelativeCall[];
27
+ malformedCount: number;
28
+ }
29
+
30
+ function record(value: unknown): Record<string, unknown> | undefined {
31
+ return value && typeof value === 'object' && !Array.isArray(value)
32
+ ? value as Record<string, unknown> : undefined;
33
+ }
34
+
35
+ function parseRecord(value: unknown): Record<string, unknown> | undefined {
36
+ if (typeof value !== 'string') return undefined;
37
+ try {
38
+ return record(JSON.parse(value) as unknown);
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ function nullableNumber(value: unknown): number | null {
45
+ return typeof value === 'number' ? value : null;
46
+ }
47
+
48
+ function all(values: readonly boolean[]): boolean {
49
+ return values.every(Boolean);
50
+ }
51
+
52
+ function memberReference(binding: SymbolImportReference): boolean {
53
+ return binding.referenceShape !== 'identifier'
54
+ && typeof binding.referencedMemberName === 'string'
55
+ && binding.referencedMemberName.length > 0;
56
+ }
57
+
58
+ function directRelativeProvenance(
59
+ evidence: Record<string, unknown>,
60
+ binding: SymbolImportReference,
61
+ targetName: string,
62
+ ): boolean {
63
+ if (evidence.relation !== 'relative_import') return false;
64
+ return binding.referenceShape !== 'namespace_member'
65
+ && targetName === binding.requestedPublicName;
66
+ }
67
+
68
+ function namespaceRelativeProvenance(
69
+ evidence: Record<string, unknown>,
70
+ binding: SymbolImportReference,
71
+ targetName: string,
72
+ ): boolean {
73
+ if (evidence.relation !== 'relative_import_namespace_member') return false;
74
+ return binding.referenceShape === 'namespace_member'
75
+ && targetName === binding.requestedPublicName;
76
+ }
77
+
78
+ function derivedRelativeProvenance(
79
+ evidence: Record<string, unknown>,
80
+ binding: SymbolImportReference,
81
+ targetName: string,
82
+ ): boolean {
83
+ if (!memberReference(binding)) return false;
84
+ if (evidence.relation === 'class_instance_method') {
85
+ if (evidence.methodName !== binding.referencedMemberName) return false;
86
+ return binding.bindingKind === 'esm_default'
87
+ ? targetName === `${binding.localName}.${binding.referencedMemberName}`
88
+ : targetName === binding.requestedPublicName;
89
+ }
90
+ if (evidence.relation !== 'relative_import_proxy_member') return false;
91
+ return targetName === binding.referencedMemberName
92
+ && typeof evidence.proxyVariableName === 'string';
93
+ }
94
+
95
+ function relativeProvenanceValid(
96
+ evidence: Record<string, unknown>,
97
+ binding: SymbolImportReference,
98
+ targetName: string,
99
+ ): boolean {
100
+ return directRelativeProvenance(evidence, binding, targetName)
101
+ || namespaceRelativeProvenance(evidence, binding, targetName)
102
+ || derivedRelativeProvenance(evidence, binding, targetName);
103
+ }
104
+
105
+ function relativeRows(
106
+ db: Db,
107
+ workspaceId?: number,
108
+ ): PersistedRelativeRows {
109
+ const rows = db.prepare(`SELECT sc.*,r.workspace_id workspaceId
110
+ FROM symbol_calls sc JOIN repositories r ON r.id=sc.repo_id
111
+ WHERE r.fact_analyzer_version=?
112
+ AND (? IS NULL OR r.workspace_id=?)
113
+ AND (sc.import_source LIKE '.%'
114
+ OR json_extract(sc.evidence_json,
115
+ '$.importBinding.moduleKind')='relative')
116
+ ORDER BY sc.id`).all(ANALYZER_VERSION, workspaceId, workspaceId);
117
+ const parsed = rows.map(persistedRelativeCall);
118
+ return {
119
+ calls: parsed.flatMap((call) => call ? [call] : []),
120
+ malformedCount: parsed.filter((call) => !call).length,
121
+ };
122
+ }
123
+
124
+ function persistedRelativeCall(
125
+ row: Record<string, unknown>,
126
+ ): PersistedRelativeCall | undefined {
127
+ const evidence = parseRecord(row.evidence_json);
128
+ const targetName = evidence?.targetName;
129
+ const binding = evidence
130
+ ? parseRelativeImportReference(evidence.importBinding)
131
+ : undefined;
132
+ if (!evidence || !binding || typeof targetName !== 'string') return undefined;
133
+ if (!all([
134
+ typeof row.repo_id === 'number',
135
+ typeof row.source_file === 'string',
136
+ typeof row.source_line === 'number',
137
+ typeof row.callee_expression === 'string',
138
+ typeof row.call_role === 'string',
139
+ typeof row.status === 'string',
140
+ typeof evidence.caller === 'string',
141
+ row.import_source === binding.rawModuleSpecifier,
142
+ relativeProvenanceValid(evidence, binding, targetName),
143
+ ])) return undefined;
144
+ return persistedCallValue(row, evidence, targetName);
145
+ }
146
+
147
+ function persistedCallValue(
148
+ row: Record<string, unknown>,
149
+ evidence: Record<string, unknown>,
150
+ targetName: string,
151
+ ): PersistedRelativeCall {
152
+ return {
153
+ repoId: Number(row.repo_id),
154
+ calleeId: nullableNumber(row.callee_symbol_id),
155
+ status: String(row.status),
156
+ reason: typeof row.unresolved_reason === 'string'
157
+ ? row.unresolved_reason : null,
158
+ fact: {
159
+ callerQualifiedName: String(evidence.caller ?? ''),
160
+ calleeExpression: String(row.callee_expression),
161
+ calleeLocalName: targetName,
162
+ importSource: String(row.import_source),
163
+ sourceFile: String(row.source_file),
164
+ sourceLine: Number(row.source_line),
165
+ callSiteStartOffset: nullableNumber(row.call_site_start_offset)
166
+ ?? undefined,
167
+ callSiteEndOffset: nullableNumber(row.call_site_end_offset)
168
+ ?? undefined,
169
+ callRole: row.call_role === 'event_subscribe_handler'
170
+ ? 'event_subscribe_handler' : 'ordinary_call',
171
+ evidence,
172
+ },
173
+ };
174
+ }
175
+
176
+ function resolutionMatches(db: Db, row: PersistedRelativeCall): boolean {
177
+ const relation = row.fact.evidence.relation;
178
+ const expected = resolveRelativeSymbolCall(
179
+ db, row.repoId, row.fact, relation,
180
+ );
181
+ if (!expected) return false;
182
+ return all([
183
+ row.status === expected.status,
184
+ row.calleeId === expected.id,
185
+ row.reason === expected.reason,
186
+ row.fact.evidence.candidateStrategy === expected.strategy,
187
+ row.fact.evidence.candidateCount === expected.candidateCount,
188
+ row.fact.evidence.eligibleCandidateCount
189
+ === expected.eligibleCandidateCount,
190
+ row.fact.evidence.selectedCandidateCount
191
+ === (expected.status === 'resolved' ? 1 : 0),
192
+ row.fact.evidence.candidateSetComplete
193
+ === expected.candidateSetComplete,
194
+ row.fact.evidence.resolvedModulePath
195
+ === expected.resolvedModulePath,
196
+ row.fact.evidence.unresolvedReason === expected.reason,
197
+ ]);
198
+ }
199
+
200
+ export function invalidRelativeFactCategories(
201
+ db: Db,
202
+ workspaceId?: number,
203
+ ): RelativeFactCategoryCount[] {
204
+ const rows = relativeRows(db, workspaceId);
205
+ const invalid = rows.malformedCount + rows.calls
206
+ .filter((row) => !resolutionMatches(db, row)).length;
207
+ return invalid > 0
208
+ ? [{ category: 'relative_import_resolution_proof_invalid', count: invalid }]
209
+ : [];
210
+ }
@@ -0,0 +1,352 @@
1
+ import type {
2
+ ServiceBindingReferenceReason,
3
+ } from '../types.js';
4
+ import { ANALYZER_VERSION } from '../version.js';
5
+ import type { Db } from './connection.js';
6
+ import {
7
+ bindingReferenceCountsValid,
8
+ resolvedBindingReferenceProofValid,
9
+ validBindingLexicalScope,
10
+ type BindingProofTarget,
11
+ } from './012-binding-reference-proof.js';
12
+ import {
13
+ hasSingleHopHelperReturn,
14
+ } from './014-binding-helper-provenance.js';
15
+
16
+ export interface BindingFactCategoryCount {
17
+ category: string;
18
+ count: number;
19
+ }
20
+
21
+ interface BindingCall {
22
+ repoId: number;
23
+ sourceSymbolId: number | null;
24
+ bindingId: number | null;
25
+ sourceFile: string;
26
+ startOffset: number;
27
+ endOffset: number;
28
+ evidence: Record<string, unknown>;
29
+ }
30
+
31
+ const unresolvedReasons = new Set<ServiceBindingReferenceReason>([
32
+ 'binding_not_found',
33
+ 'binding_declared_after_call',
34
+ 'scope_chain_limit_exceeded',
35
+ 'unsupported_reaching_assignment',
36
+ 'unsupported_var_binding',
37
+ 'binding_flow_unsupported',
38
+ ]);
39
+ const ownerKinds =
40
+ "'event_registration','callback','method','object_method','function'";
41
+ const bindingOwnerSql = `WITH eligible AS (
42
+ SELECT binding.id binding_id,s.id symbol_id,
43
+ DENSE_RANK() OVER (PARTITION BY binding.id ORDER BY
44
+ s.end_offset-s.start_offset,
45
+ CASE s.kind WHEN 'event_registration' THEN 0 WHEN 'callback' THEN 1
46
+ WHEN 'method' THEN 2 WHEN 'object_method' THEN 3 ELSE 4 END,
47
+ s.start_offset,s.end_offset,s.qualified_name COLLATE BINARY) owner_rank
48
+ FROM service_bindings binding
49
+ JOIN repositories repo ON repo.id=binding.repo_id
50
+ JOIN symbols s ON s.repo_id=binding.repo_id
51
+ AND s.source_file=binding.source_file
52
+ AND s.kind IN (${ownerKinds})
53
+ AND s.start_offset<=binding.binding_site_start_offset
54
+ AND s.end_offset>=binding.binding_site_end_offset
55
+ WHERE repo.fact_analyzer_version=?
56
+ AND (? IS NULL OR repo.workspace_id=?)
57
+ ), best AS (
58
+ SELECT binding_id,COUNT(*) best_count,MAX(symbol_id) symbol_id
59
+ FROM eligible WHERE owner_rank=1 GROUP BY binding_id
60
+ )
61
+ SELECT COUNT(*) count FROM service_bindings binding
62
+ JOIN repositories repo ON repo.id=binding.repo_id
63
+ LEFT JOIN best ON best.binding_id=binding.id
64
+ WHERE repo.fact_analyzer_version=?
65
+ AND (? IS NULL OR repo.workspace_id=?)
66
+ AND ((best.binding_id IS NULL AND (
67
+ binding.symbol_id IS NOT NULL
68
+ OR binding.owner_resolution<>'ownerless_file_scope'))
69
+ OR (best.binding_id IS NOT NULL AND (
70
+ best.best_count<>1 OR binding.symbol_id IS NOT best.symbol_id
71
+ OR binding.owner_resolution<>'owned_exact')))`;
72
+
73
+ function record(value: unknown): Record<string, unknown> | undefined {
74
+ return value && typeof value === 'object' && !Array.isArray(value)
75
+ ? value as Record<string, unknown> : undefined;
76
+ }
77
+
78
+ function parseRecord(value: unknown): Record<string, unknown> | undefined {
79
+ if (typeof value !== 'string') return undefined;
80
+ try {
81
+ return record(JSON.parse(value) as unknown);
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ function integer(value: unknown): value is number {
88
+ return Number.isInteger(value);
89
+ }
90
+
91
+ function nonEmpty(value: unknown): value is string {
92
+ return typeof value === 'string' && value.length > 0;
93
+ }
94
+
95
+ function all(values: readonly boolean[]): boolean {
96
+ return values.every(Boolean);
97
+ }
98
+
99
+ function currentParameters(workspaceId?: number): unknown[] {
100
+ return [ANALYZER_VERSION, workspaceId, workspaceId];
101
+ }
102
+
103
+ function category(
104
+ name: string,
105
+ count: number,
106
+ ): BindingFactCategoryCount[] {
107
+ return count > 0 ? [{ category: name, count }] : [];
108
+ }
109
+
110
+ function bindingTargets(
111
+ db: Db,
112
+ workspaceId?: number,
113
+ ): Map<number, BindingProofTarget> {
114
+ const rows = db.prepare(`SELECT binding.id,binding.repo_id repoId,
115
+ binding.symbol_id symbolId,binding.variable_name variableName,
116
+ binding.source_file sourceFile,
117
+ binding.binding_site_start_offset startOffset,
118
+ binding.binding_site_end_offset endOffset,
119
+ binding.owner_resolution ownerResolution,
120
+ binding.helper_chain_json helperChainJson,
121
+ owner.start_offset ownerStartOffset,owner.end_offset ownerEndOffset
122
+ FROM service_bindings binding
123
+ JOIN repositories repo ON repo.id=binding.repo_id
124
+ LEFT JOIN symbols owner ON owner.id=binding.symbol_id
125
+ WHERE repo.fact_analyzer_version=?
126
+ AND (? IS NULL OR repo.workspace_id=?)`).all(
127
+ ...currentParameters(workspaceId),
128
+ );
129
+ return new Map(rows.flatMap(bindingTarget).map((item) => [item.id, item]));
130
+ }
131
+
132
+ function bindingTarget(
133
+ row: Record<string, unknown>,
134
+ ): BindingProofTarget[] {
135
+ if (!bindingTargetShapeValid(row)) return [];
136
+ return [{
137
+ id: row.id,
138
+ repoId: row.repoId,
139
+ symbolId: row.symbolId,
140
+ variableName: row.variableName,
141
+ sourceFile: row.sourceFile,
142
+ startOffset: row.startOffset,
143
+ endOffset: row.endOffset,
144
+ ownerResolution: row.ownerResolution,
145
+ ownerStartOffset: row.ownerStartOffset,
146
+ ownerEndOffset: row.ownerEndOffset,
147
+ singleHopHelperReturn: hasSingleHopHelperReturn(row.helperChainJson),
148
+ }];
149
+ }
150
+
151
+ function nullableInteger(value: unknown): boolean {
152
+ return value === null || integer(value);
153
+ }
154
+
155
+ function bindingTargetShapeValid(
156
+ row: Record<string, unknown>,
157
+ ): row is Record<string, unknown> & {
158
+ id: number;
159
+ repoId: number;
160
+ symbolId: number | null;
161
+ variableName: string;
162
+ sourceFile: string;
163
+ startOffset: number;
164
+ endOffset: number;
165
+ ownerResolution: string;
166
+ ownerStartOffset: number | null;
167
+ ownerEndOffset: number | null;
168
+ } {
169
+ return all([
170
+ integer(row.id), integer(row.repoId),
171
+ nonEmpty(row.variableName), nonEmpty(row.sourceFile),
172
+ integer(row.startOffset), integer(row.endOffset),
173
+ typeof row.ownerResolution === 'string',
174
+ nullableInteger(row.symbolId),
175
+ nullableInteger(row.ownerStartOffset),
176
+ nullableInteger(row.ownerEndOffset),
177
+ ]);
178
+ }
179
+
180
+ function bindingCalls(db: Db, workspaceId?: number): BindingCall[] {
181
+ const rows = db.prepare(`SELECT call.repo_id repoId,
182
+ call.source_symbol_id sourceSymbolId,
183
+ call.service_binding_id bindingId,call.source_file sourceFile,
184
+ call.call_site_start_offset startOffset,
185
+ call.call_site_end_offset endOffset,call.evidence_json evidenceJson
186
+ FROM outbound_calls call JOIN repositories repo ON repo.id=call.repo_id
187
+ WHERE repo.fact_analyzer_version=?
188
+ AND (? IS NULL OR repo.workspace_id=?)`).all(
189
+ ...currentParameters(workspaceId),
190
+ );
191
+ return rows.flatMap(bindingCall);
192
+ }
193
+
194
+ function currentFactCount(
195
+ db: Db,
196
+ table: 'service_bindings' | 'outbound_calls',
197
+ workspaceId?: number,
198
+ ): number {
199
+ const row = db.prepare(`SELECT COUNT(*) count FROM ${table} fact
200
+ JOIN repositories repo ON repo.id=fact.repo_id
201
+ WHERE repo.fact_analyzer_version=?
202
+ AND (? IS NULL OR repo.workspace_id=?)`).get(
203
+ ...currentParameters(workspaceId),
204
+ );
205
+ return Number(row?.count ?? 0);
206
+ }
207
+
208
+ function bindingCall(row: Record<string, unknown>): BindingCall[] {
209
+ const evidence = parseRecord(row.evidenceJson);
210
+ if (!integer(row.repoId) || !nonEmpty(row.sourceFile)
211
+ || !integer(row.startOffset) || !integer(row.endOffset) || !evidence
212
+ || !(row.sourceSymbolId === null || integer(row.sourceSymbolId))
213
+ || !(row.bindingId === null || integer(row.bindingId))) return [];
214
+ return [{
215
+ repoId: row.repoId,
216
+ sourceSymbolId: row.sourceSymbolId,
217
+ bindingId: row.bindingId,
218
+ sourceFile: row.sourceFile,
219
+ startOffset: row.startOffset,
220
+ endOffset: row.endOffset,
221
+ evidence,
222
+ }];
223
+ }
224
+
225
+ function rejectedFieldsAbsent(
226
+ reference: Record<string, unknown>,
227
+ ): boolean {
228
+ return all([
229
+ reference.bindingSourceFile === undefined,
230
+ reference.bindingSiteStartOffset === undefined,
231
+ reference.bindingSiteEndOffset === undefined,
232
+ reference.resolutionStrategy === undefined,
233
+ reference.bindingScopeIndex === undefined,
234
+ ]);
235
+ }
236
+
237
+ function rejectedChainValid(
238
+ reference: Record<string, unknown>,
239
+ ): boolean {
240
+ const chain = reference.lexicalScopeChain;
241
+ if (!bindingReferenceCountsValid(reference)) return false;
242
+ if (Array.isArray(chain) && !chain.every(validBindingLexicalScope))
243
+ return false;
244
+ return !Array.isArray(chain)
245
+ || reference.scopeChainShown === chain.length;
246
+ }
247
+
248
+ function rejectedReasonValid(
249
+ reference: Record<string, unknown>,
250
+ ): boolean {
251
+ const reason = reference.reason as ServiceBindingReferenceReason;
252
+ const limitExceeded = reason === 'scope_chain_limit_exceeded';
253
+ if (limitExceeded
254
+ ? reference.scopeChainShown !== 16
255
+ || Number(reference.scopeChainOmitted) <= 0
256
+ : reference.scopeChainOmitted !== 0) return false;
257
+ if (reference.status === 'ambiguous')
258
+ return reason === 'binding_scope_ambiguous';
259
+ return reference.status === 'unresolved' && unresolvedReasons.has(reason);
260
+ }
261
+
262
+ function rejectedShape(
263
+ call: BindingCall,
264
+ reference: Record<string, unknown>,
265
+ ): boolean {
266
+ return all([
267
+ nonEmpty(reference.variableName),
268
+ call.bindingId === null,
269
+ rejectedFieldsAbsent(reference),
270
+ rejectedChainValid(reference),
271
+ rejectedReasonValid(reference),
272
+ ]);
273
+ }
274
+
275
+ function notApplicableShape(
276
+ call: BindingCall,
277
+ reference: Record<string, unknown>,
278
+ ): boolean {
279
+ return all([
280
+ call.bindingId === null,
281
+ reference.variableName === undefined,
282
+ reference.reason === undefined,
283
+ rejectedFieldsAbsent(reference),
284
+ reference.lexicalScopeChain === undefined,
285
+ bindingReferenceCountsValid(reference),
286
+ reference.scopeChainTotal === 0,
287
+ reference.scopeChainShown === 0,
288
+ reference.scopeChainOmitted === 0,
289
+ ]);
290
+ }
291
+
292
+ function resolutionEvidenceValid(
293
+ call: BindingCall,
294
+ reference: Record<string, unknown>,
295
+ ): boolean {
296
+ const resolution = record(call.evidence.serviceBindingResolution);
297
+ if (!resolution) return false;
298
+ if (reference.status === 'resolved_exact')
299
+ return resolution.status === 'selected_exact'
300
+ && resolution.selectedBindingId === call.bindingId
301
+ && resolution.candidateCount === 1;
302
+ return resolution.status === reference.status
303
+ && resolution.candidateCount === 0
304
+ && resolution.selectedBindingId === undefined;
305
+ }
306
+
307
+ function referenceValid(
308
+ call: BindingCall,
309
+ targets: ReadonlyMap<number, BindingProofTarget>,
310
+ ): boolean {
311
+ const reference = record(call.evidence.serviceBindingReference);
312
+ if (!reference || !resolutionEvidenceValid(call, reference)) return false;
313
+ if (reference.status === 'not_applicable')
314
+ return notApplicableShape(call, reference);
315
+ if (reference.status === 'ambiguous' || reference.status === 'unresolved')
316
+ return rejectedShape(call, reference);
317
+ if (reference.status !== 'resolved_exact') return false;
318
+ return resolvedBindingReferenceProofValid(
319
+ call,
320
+ reference,
321
+ call.bindingId === null ? undefined : targets.get(call.bindingId),
322
+ );
323
+ }
324
+
325
+ function invalidOwnerCount(db: Db, workspaceId?: number): number {
326
+ const params = currentParameters(workspaceId);
327
+ const row = db.prepare(bindingOwnerSql).get(...params, ...params);
328
+ return Number(row?.count ?? 0);
329
+ }
330
+
331
+ export function invalidBindingFactCategories(
332
+ db: Db,
333
+ workspaceId?: number,
334
+ ): BindingFactCategoryCount[] {
335
+ const targets = bindingTargets(db, workspaceId);
336
+ const calls = bindingCalls(db, workspaceId);
337
+ const invalidReferences = calls.filter((call) =>
338
+ !referenceValid(call, targets)).length;
339
+ const malformedTargets = currentFactCount(
340
+ db, 'service_bindings', workspaceId,
341
+ ) - targets.size;
342
+ const malformedCalls = currentFactCount(
343
+ db, 'outbound_calls', workspaceId,
344
+ ) - calls.length;
345
+ return [
346
+ ...category('service_binding_row_shape_invalid', malformedTargets),
347
+ ...category('outbound_binding_row_shape_invalid', malformedCalls),
348
+ ...category('service_binding_unique_owner_invalid',
349
+ invalidOwnerCount(db, workspaceId)),
350
+ ...category('outbound_binding_lexical_proof_invalid', invalidReferences),
351
+ ];
352
+ }