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