@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
@@ -1,17 +1,38 @@
1
- import { posix } from 'node:path';
2
1
  import type { OutboundCallFact, SymbolCallFact } from '../types.js';
3
- import { projectBounded } from '../utils/000-bounded-projection.js';
2
+ import {
3
+ selectCallOwner,
4
+ type OwnerCandidate,
5
+ type OwnerSelection,
6
+ } from '../parsers/004-fact-identity.js';
4
7
  import type { Db, Statement } from './connection.js';
8
+ import {
9
+ resolveRelativeSymbolCall,
10
+ } from './006-relative-symbol-resolution.js';
11
+ import {
12
+ parsePackageImportReference,
13
+ } from '../parsers/012-package-fact-contract.js';
14
+ import {
15
+ resolvedBindingReferenceProofValid,
16
+ type BindingProofCall,
17
+ type BindingProofTarget,
18
+ } from './012-binding-reference-proof.js';
19
+ import {
20
+ preparedCallSnapshotError,
21
+ } from './013-index-publication-failure.js';
22
+ import {
23
+ hasSingleHopHelperReturn,
24
+ } from './014-binding-helper-provenance.js';
5
25
 
6
26
  export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]): void {
7
- const callerStmt = db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1');
8
27
  const insertStmt = db.prepare('INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,call_site_start_offset,call_site_end_offset,call_role,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
9
28
  for (const r of rows) {
10
- const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName) as { id?: number } | undefined;
29
+ assertImportProvenance(r);
30
+ const callerId = requiredSymbolCallOwnerId(db, repoId, r);
11
31
  const target = resolveSymbolCallTarget(db, repoId, r);
32
+ const cardinality = resolutionCardinality(target);
12
33
  insertStmt.run(
13
34
  repoId,
14
- caller?.id,
35
+ callerId,
15
36
  target.id,
16
37
  r.calleeExpression,
17
38
  r.importSource,
@@ -26,6 +47,10 @@ export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]
26
47
  ...r.evidence,
27
48
  candidateStrategy: target.strategy,
28
49
  candidateCount: target.candidateCount,
50
+ eligibleCandidateCount: cardinality.eligibleCandidateCount,
51
+ selectedCandidateCount: cardinality.selectedCandidateCount,
52
+ candidateSetComplete: cardinality.candidateSetComplete,
53
+ unresolvedReason: target.reason,
29
54
  resolvedModulePath: target.resolvedModulePath,
30
55
  }),
31
56
  target.reason,
@@ -33,6 +58,40 @@ export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]
33
58
  }
34
59
  }
35
60
 
61
+ function assertImportProvenance(call: SymbolCallFact): void {
62
+ if (call.importSource === undefined || call.importSource.startsWith('.'))
63
+ return;
64
+ const direct = directPackageProvenanceValid(call);
65
+ const derived = derivedPackageProvenanceValid(call);
66
+ if (!direct && !derived)
67
+ throw preparedCallSnapshotError(
68
+ 'package_import_provenance_missing', 'symbol_call', call,
69
+ );
70
+ }
71
+
72
+ function directPackageProvenanceValid(call: SymbolCallFact): boolean {
73
+ const binding = parsePackageImportReference(call.evidence.importBinding);
74
+ return Boolean(binding
75
+ && call.evidence.relation === 'package_import'
76
+ && call.evidence.derivedImportBinding === undefined
77
+ && call.importSource === binding.rawModuleSpecifier
78
+ && call.evidence.targetName === binding.requestedPublicName);
79
+ }
80
+
81
+ function derivedPackageProvenanceValid(call: SymbolCallFact): boolean {
82
+ const binding = parsePackageImportReference(
83
+ call.evidence.derivedImportBinding,
84
+ );
85
+ if (!binding || typeof binding.referencedMemberName !== 'string')
86
+ return false;
87
+ const expected = typeof call.evidence.proxyVariableName === 'string'
88
+ ? binding.referencedMemberName : binding.requestedPublicName;
89
+ return call.evidence.relation === 'package_import_derived_member'
90
+ && call.evidence.importBinding === undefined
91
+ && call.importSource === binding.rawModuleSpecifier
92
+ && call.evidence.targetName === expected;
93
+ }
94
+
36
95
  interface SymbolTargetRow {
37
96
  id: number;
38
97
  kind?: string;
@@ -46,38 +105,116 @@ interface SymbolCallResolution {
46
105
  reason: string | null;
47
106
  strategy: string;
48
107
  candidateCount: number;
108
+ eligibleCandidateCount: number;
109
+ candidateSetComplete: boolean;
49
110
  resolvedModulePath?: string;
50
111
  }
51
112
 
52
- const stripExt = (value: string): string => value.replace(/\.(ts|tsx|js|jsx|cds)$/, '');
113
+ interface PersistedOwnerCandidate extends OwnerCandidate {
114
+ id: number;
115
+ }
116
+
117
+ function persistedCallOwners(
118
+ db: Db,
119
+ repoId: number,
120
+ sourceFile: string,
121
+ start: number | undefined,
122
+ end: number | undefined,
123
+ ): PersistedOwnerCandidate[] {
124
+ if (start === undefined || end === undefined) return [];
125
+ const rows = db.prepare(`SELECT id,kind,qualified_name qualifiedName,
126
+ start_offset startOffset,end_offset endOffset FROM symbols
127
+ WHERE repo_id=? AND source_file=? AND start_offset<=? AND end_offset>=?`)
128
+ .all(repoId, sourceFile, start, end);
129
+ return rows.flatMap(persistedOwnerCandidate);
130
+ }
131
+
132
+ function persistedOwnerCandidate(
133
+ row: Record<string, unknown>,
134
+ ): PersistedOwnerCandidate[] {
135
+ if (typeof row.id !== 'number' || typeof row.kind !== 'string'
136
+ || typeof row.qualifiedName !== 'string'
137
+ || typeof row.startOffset !== 'number'
138
+ || typeof row.endOffset !== 'number') return [];
139
+ return [{
140
+ id: row.id,
141
+ kind: row.kind,
142
+ qualifiedName: row.qualifiedName,
143
+ startOffset: row.startOffset,
144
+ endOffset: row.endOffset,
145
+ }];
146
+ }
147
+
148
+ function requiredSymbolCallOwnerId(
149
+ db: Db,
150
+ repoId: number,
151
+ call: SymbolCallFact,
152
+ ): number {
153
+ const candidates = persistedCallOwners(
154
+ db, repoId, call.sourceFile,
155
+ call.callSiteStartOffset, call.callSiteEndOffset,
156
+ );
157
+ const selected = selectCallOwner(
158
+ candidates,
159
+ call.callSiteStartOffset ?? -1,
160
+ call.callSiteEndOffset ?? -1,
161
+ call.callRole === 'event_subscribe_handler',
162
+ );
163
+ if (selected.status !== 'resolved'
164
+ || selected.owner?.qualifiedName !== call.callerQualifiedName)
165
+ throw preparedCallSnapshotError(
166
+ 'symbol_call_owner_mismatch', 'symbol_call', call,
167
+ );
168
+ return selected.owner.id;
169
+ }
170
+
171
+ function resolutionCardinality(
172
+ target: SymbolCallResolution,
173
+ ): {
174
+ eligibleCandidateCount: number;
175
+ selectedCandidateCount: 0 | 1;
176
+ candidateSetComplete: boolean;
177
+ } {
178
+ return {
179
+ eligibleCandidateCount: target.eligibleCandidateCount,
180
+ selectedCandidateCount: target.status === 'resolved' ? 1 : 0,
181
+ candidateSetComplete: target.candidateSetComplete,
182
+ };
183
+ }
53
184
 
54
185
  function symbolTargetRows(rows: Array<Record<string, unknown>>): SymbolTargetRow[] {
55
186
  return rows.flatMap((row) => typeof row.id === 'number' ? [{
56
187
  id: row.id,
57
188
  kind: typeof row.kind === 'string' ? row.kind : undefined,
58
- sourceFile: nullableString(row.sourceFile),
59
- evidenceJson: nullableString(row.evidenceJson),
189
+ sourceFile: typeof row.sourceFile === 'string' ? row.sourceFile : null,
190
+ evidenceJson: typeof row.evidenceJson === 'string'
191
+ ? row.evidenceJson : null,
60
192
  }] : []);
61
193
  }
62
194
 
63
- function relativeModuleTargets(callerSourceFile: string, importSource: string): Set<string> {
64
- const base = posix.dirname(callerSourceFile);
65
- const joined = stripExt(posix.normalize(posix.join(base, importSource)));
66
- return new Set([joined, `${joined}/index`]);
195
+ function record(value: unknown): Record<string, unknown> | undefined {
196
+ return value && typeof value === 'object' && !Array.isArray(value)
197
+ ? value as Record<string, unknown> : undefined;
67
198
  }
68
199
 
69
- function moduleRows(rows: SymbolTargetRow[], r: SymbolCallFact): SymbolTargetRow[] {
70
- if (!r.importSource) return [];
71
- const targets = relativeModuleTargets(r.sourceFile, r.importSource);
72
- return rows.filter((row) => typeof row.sourceFile === 'string'
73
- && targets.has(stripExt(row.sourceFile)));
200
+ function executableTarget(row: SymbolTargetRow): boolean {
201
+ if (!row.evidenceJson || !row.kind
202
+ || !['function', 'method', 'object_method', 'callback'].includes(row.kind))
203
+ return false;
204
+ try {
205
+ const evidence = record(JSON.parse(row.evidenceJson) as unknown);
206
+ const body = record(evidence?.executableBodyEligibility);
207
+ return body?.eligible === true && body.reason === 'body_present';
208
+ } catch {
209
+ return false;
210
+ }
74
211
  }
75
212
 
76
213
  function resolvedSymbol(
77
214
  row: SymbolTargetRow,
78
215
  strategy: string,
79
216
  candidateCount: number,
80
- moduleScoped = false,
217
+ eligibleCandidateCount = 1,
81
218
  ): SymbolCallResolution {
82
219
  return {
83
220
  id: row.id,
@@ -85,211 +222,170 @@ function resolvedSymbol(
85
222
  reason: null,
86
223
  strategy,
87
224
  candidateCount,
88
- resolvedModulePath: moduleScoped && row.sourceFile
89
- ? stripExt(row.sourceFile)
90
- : undefined,
225
+ eligibleCandidateCount,
226
+ candidateSetComplete: true,
91
227
  };
92
228
  }
93
229
 
94
- function exportedSymbolRows(db: Db, repoId: number, r: SymbolCallFact): SymbolTargetRow[] {
95
- return symbolTargetRows(db.prepare('SELECT id,kind,source_file sourceFile,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName));
96
- }
97
-
98
- function isRelativeImportedSymbolCall(r: SymbolCallFact): boolean {
99
- return Boolean(r.importSource?.startsWith('.'));
100
- }
101
-
102
- function sameFileResolution(
103
- db: Db,
104
- repoId: number,
105
- r: SymbolCallFact,
106
- relation: unknown,
107
- ): SymbolCallResolution | undefined {
108
- const bareImport = relation === 'relative_import' && isRelativeImportedSymbolCall(r)
109
- && !String(r.calleeLocalName).includes('.');
110
- if (bareImport || relation === 'relative_import_namespace_member'
111
- || relation === 'package_import') return undefined;
112
- const rows = symbolTargetRows(db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName));
113
- if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'same_file_exact', 1);
114
- return rows.length > 1
115
- ? {
116
- id: null,
117
- status: 'ambiguous',
118
- reason: 'Multiple same-file symbol targets matched exactly',
119
- strategy: 'same_file_exact',
120
- candidateCount: rows.length,
121
- }
122
- : undefined;
123
- }
124
-
125
- function classInstanceResolution(
126
- db: Db,
127
- repoId: number,
128
- r: SymbolCallFact,
129
- relation: unknown,
130
- ): SymbolCallResolution | undefined {
131
- if (relation !== 'class_instance_method' || !isRelativeImportedSymbolCall(r))
132
- return undefined;
133
- const rows = symbolTargetRows(db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName));
134
- if (rows.length === 1 && rows[0])
135
- return resolvedSymbol(rows[0], 'relative_import_class_instance_method', 1);
136
- return rows.length > 1
137
- ? {
138
- id: null,
139
- status: 'ambiguous',
140
- reason: 'Multiple relative class instance method targets matched exactly',
141
- strategy: 'relative_import_class_instance_method',
142
- candidateCount: rows.length,
143
- }
144
- : undefined;
145
- }
146
-
147
- function namespaceResolution(
148
- db: Db,
149
- repoId: number,
150
- r: SymbolCallFact,
151
- relation: unknown,
152
- ): SymbolCallResolution | undefined {
153
- if (relation !== 'relative_import_namespace_member'
154
- || !isRelativeImportedSymbolCall(r)) return undefined;
155
- const rows = moduleRows(exportedSymbolRows(db, repoId, r), r);
156
- if (rows.length === 1 && rows[0])
157
- return resolvedSymbol(rows[0], 'relative_import_namespace_member', 1, true);
158
- if (rows.length > 1) return {
159
- id: null,
160
- status: 'ambiguous',
161
- reason: 'Multiple namespace member targets matched the imported module',
162
- strategy: 'relative_import_namespace_member',
163
- candidateCount: rows.length,
164
- };
230
+ function unresolvedSymbol(
231
+ strategy: string,
232
+ reason: string,
233
+ candidateCount: number,
234
+ eligibleCandidateCount = 0,
235
+ candidateSetComplete = true,
236
+ ): SymbolCallResolution {
165
237
  return {
166
238
  id: null,
167
239
  status: 'unresolved',
168
- reason: 'No namespace member target matched the imported module',
169
- strategy: 'relative_import_namespace_member',
170
- candidateCount: 0,
240
+ reason,
241
+ strategy,
242
+ candidateCount,
243
+ eligibleCandidateCount,
244
+ candidateSetComplete,
171
245
  };
172
246
  }
173
247
 
174
- function proxyResolution(
175
- rows: SymbolTargetRow[],
176
- r: SymbolCallFact,
177
- relation: unknown,
178
- ): SymbolCallResolution | undefined {
179
- if (relation !== 'relative_import_proxy_member' || rows.length <= 1) return undefined;
180
- const mapped = rows.filter(isExportedObjectMapping);
181
- if (mapped.length > 0) {
182
- const concrete = rows.find((row) => row.kind !== 'object_alias') ?? mapped[0];
183
- return {
184
- id: concrete?.id ?? null,
185
- status: 'resolved',
186
- reason: null,
187
- strategy: 'proxy_member_exported_object_map',
188
- candidateCount: rows.length,
189
- };
190
- }
191
- const scoped = moduleRows(rows, r);
192
- if (scoped.length === 1 && scoped[0])
193
- return resolvedSymbol(scoped[0], 'relative_import_path_disambiguated', rows.length, true);
248
+ function ambiguousSymbol(
249
+ strategy: string,
250
+ reason: string,
251
+ candidateCount: number,
252
+ eligibleCandidateCount: number,
253
+ ): SymbolCallResolution {
194
254
  return {
195
255
  id: null,
196
256
  status: 'ambiguous',
197
- reason: 'Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous',
198
- strategy: 'proxy_member_no_global_name_fallback',
199
- candidateCount: rows.length,
257
+ reason,
258
+ strategy,
259
+ candidateCount,
260
+ eligibleCandidateCount,
261
+ candidateSetComplete: true,
200
262
  };
201
263
  }
202
264
 
203
- function isExportedObjectMapping(row: SymbolTargetRow): boolean {
204
- const evidence = String(row.evidenceJson ?? '');
205
- return evidence.includes('exported_object_shorthand')
206
- || evidence.includes('exported_object_literal');
265
+ function exportedSymbolRows(db: Db, repoId: number, r: SymbolCallFact): SymbolTargetRow[] {
266
+ return symbolTargetRows(db.prepare(`SELECT id,kind,
267
+ source_file sourceFile,evidence_json evidenceJson FROM symbols
268
+ WHERE repo_id=? AND source_file<>? AND exported=1
269
+ AND (exported_name=? OR name=? OR qualified_name=?)
270
+ ORDER BY id`).all(
271
+ repoId, r.sourceFile,
272
+ r.calleeLocalName, r.calleeLocalName, r.calleeLocalName,
273
+ ));
207
274
  }
208
275
 
209
- function exportedResolution(
276
+ const sameFileEligibleRelations = new Set([
277
+ 'indexed_local_symbol',
278
+ 'indexed_local_symbol_unproven',
279
+ 'indexed_this_method',
280
+ ]);
281
+
282
+ function eligibleSymbolResolution(
210
283
  rows: SymbolTargetRow[],
211
- r: SymbolCallFact,
212
- relation: unknown,
284
+ strategy: string,
285
+ ambiguousReason: string,
213
286
  ): SymbolCallResolution | undefined {
214
- if (rows.length === 1 && rows[0]) return resolvedSymbol(
215
- rows[0],
216
- relation === 'relative_import_proxy_member'
217
- ? 'proxy_member_unique_exported_candidate'
218
- : 'relative_import_exported_exact',
219
- 1,
220
- moduleRows(rows, r).length === 1,
287
+ if (rows.length === 0) return undefined;
288
+ const eligible = rows.filter(executableTarget);
289
+ if (eligible.length === 1 && eligible[0])
290
+ return resolvedSymbol(eligible[0], strategy, rows.length, 1);
291
+ if (eligible.length > 1) return ambiguousSymbol(
292
+ strategy, ambiguousReason, rows.length, eligible.length,
293
+ );
294
+ return unresolvedSymbol(
295
+ strategy, 'symbol_target_has_no_executable_body', rows.length,
221
296
  );
222
- if (rows.length <= 1) return undefined;
223
- const scoped = isRelativeImportedSymbolCall(r) ? moduleRows(rows, r) : [];
224
- if (scoped.length === 1 && scoped[0])
225
- return resolvedSymbol(scoped[0], 'relative_import_path_disambiguated', rows.length, true);
226
- return {
227
- id: null,
228
- status: 'ambiguous',
229
- reason: 'Multiple exported symbol targets matched exactly',
230
- strategy: 'exported_exact',
231
- candidateCount: rows.length,
232
- };
233
297
  }
234
298
 
235
- function accessorResolution(
299
+ function sameFileResolution(
236
300
  db: Db,
237
301
  repoId: number,
238
302
  r: SymbolCallFact,
239
303
  relation: unknown,
240
304
  ): SymbolCallResolution | undefined {
241
- if (relation !== 'relative_import' || !isRelativeImportedSymbolCall(r)
242
- || !/^[^.]+\.[^.]+$/.test(String(r.calleeLocalName))) return undefined;
243
- const methodRows = symbolTargetRows(db.prepare("SELECT id,kind,source_file sourceFile FROM symbols WHERE repo_id=? AND source_file<>? AND kind='method' AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName));
244
- const scoped = moduleRows(methodRows, r);
245
- if (scoped.length === 1 && scoped[0]) return resolvedSymbol(
246
- scoped[0],
247
- 'relative_import_static_accessor_instance_method',
248
- 1,
249
- true,
305
+ if (!sameFileEligibleRelations.has(String(relation))) return undefined;
306
+ if (relation === 'indexed_local_symbol_unproven')
307
+ return unresolvedSymbol(
308
+ 'exact_symbol_match', 'no_local_symbol_target', 0,
309
+ );
310
+ if (relation === 'indexed_local_symbol'
311
+ && r.evidence.localTargetIdentity !== undefined)
312
+ return exactLocalSymbolResolution(db, repoId, r);
313
+ const rows = symbolTargetRows(db.prepare(`SELECT id,kind,
314
+ source_file sourceFile,evidence_json evidenceJson FROM symbols
315
+ WHERE repo_id=? AND source_file=?
316
+ AND (name=? OR qualified_name=?) ORDER BY id`).all(
317
+ repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName,
318
+ ));
319
+ return eligibleSymbolResolution(
320
+ rows, 'same_file_exact', 'multiple_same_file_symbol_targets',
250
321
  );
251
- return scoped.length > 1
252
- ? {
253
- id: null,
254
- status: 'ambiguous',
255
- reason: 'Multiple static-accessor instance method targets matched the imported module',
256
- strategy: 'relative_import_static_accessor_instance_method',
257
- candidateCount: scoped.length,
258
- }
259
- : undefined;
260
- }
261
-
262
- function resolveSymbolCallTarget(
322
+ }
323
+
324
+ function exactLocalSymbolResolution(
325
+ db: Db,
326
+ repoId: number,
327
+ call: SymbolCallFact,
328
+ ): SymbolCallResolution {
329
+ const target = record(call.evidence.localTargetIdentity);
330
+ if (!target
331
+ || typeof target.sourceFile !== 'string'
332
+ || typeof target.qualifiedName !== 'string'
333
+ || typeof target.startOffset !== 'number'
334
+ || typeof target.endOffset !== 'number')
335
+ return unresolvedSymbol(
336
+ 'exact_symbol_match', 'no_local_symbol_target', 0,
337
+ );
338
+ const rows = symbolTargetRows(db.prepare(`SELECT id,kind,
339
+ source_file sourceFile,evidence_json evidenceJson FROM symbols
340
+ WHERE repo_id=? AND source_file=? AND qualified_name=?
341
+ AND start_offset=? AND end_offset=? ORDER BY id`).all(
342
+ repoId, target.sourceFile, target.qualifiedName,
343
+ target.startOffset, target.endOffset,
344
+ ));
345
+ return eligibleSymbolResolution(
346
+ rows, 'same_file_exact', 'multiple_same_file_symbol_targets',
347
+ ) ?? unresolvedSymbol(
348
+ 'exact_symbol_match', 'no_local_symbol_target', 0,
349
+ );
350
+ }
351
+
352
+ function exportedResolution(
353
+ rows: SymbolTargetRow[],
354
+ ): SymbolCallResolution | undefined {
355
+ return eligibleSymbolResolution(
356
+ rows, 'exported_exact', 'multiple_exported_symbol_targets',
357
+ );
358
+ }
359
+
360
+ export function resolveSymbolCallTarget(
263
361
  db: Db,
264
362
  repoId: number,
265
363
  r: SymbolCallFact,
266
364
  ): SymbolCallResolution {
267
365
  const relation = r.evidence.relation;
268
- const early = sameFileResolution(db, repoId, r, relation)
269
- ?? classInstanceResolution(db, repoId, r, relation)
270
- ?? namespaceResolution(db, repoId, r, relation);
366
+ const relative = resolveRelativeSymbolCall(db, repoId, r, relation);
367
+ if (relative) return relative;
368
+ const early = sameFileResolution(db, repoId, r, relation);
271
369
  if (early) return early;
272
- const rows = relation === 'package_import' ? [] : exportedSymbolRows(db, repoId, r);
273
- const matched = proxyResolution(rows, r, relation)
274
- ?? exportedResolution(rows, r, relation)
275
- ?? accessorResolution(db, repoId, r, relation);
370
+ if (relation === 'package_import_derived_member') return unresolvedSymbol(
371
+ 'package_import_derived_member_unsupported',
372
+ 'package_derived_member_provenance_insufficient',
373
+ 0,
374
+ );
375
+ if (relation === 'package_import') return unresolvedSymbol(
376
+ 'package_import_pending',
377
+ 'package_resolution_pending',
378
+ 0,
379
+ 0,
380
+ false,
381
+ );
382
+ const matched = exportedResolution(exportedSymbolRows(db, repoId, r));
276
383
  if (matched) return matched;
277
- if (relation === 'package_import') return {
278
- id: null,
279
- status: 'unresolved',
280
- reason: 'Package import target resolution requires a post-publication workspace pass',
281
- strategy: 'package_import_unresolved',
282
- candidateCount: 0,
283
- };
284
- return {
285
- id: null,
286
- status: 'unresolved',
287
- reason: 'No local symbol target matched exactly',
288
- strategy: relation === 'relative_import_proxy_member'
289
- ? 'proxy_member_no_global_name_fallback'
290
- : 'exact_symbol_match',
291
- candidateCount: 0,
292
- };
384
+ return unresolvedSymbol(
385
+ 'exact_symbol_match',
386
+ 'no_local_symbol_target',
387
+ 0,
388
+ );
293
389
  }
294
390
 
295
391
  export function insertCalls(
@@ -308,12 +404,7 @@ function outboundCallInsertStatement(db: Db): Statement {
308
404
  call_site_end_offset,confidence,unresolved_reason,local_service_name,
309
405
  local_service_lookup,alias_chain_json,evidence_json,external_target_kind,
310
406
  external_target_id,external_target_label,external_target_dynamic,service_binding_id
311
- ) VALUES(
312
- ?,COALESCE(
313
- (SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),
314
- (SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)
315
- ),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
316
- )`);
407
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
317
408
  }
318
409
 
319
410
  function insertOutboundCall(
@@ -322,19 +413,20 @@ function insertOutboundCall(
322
413
  repoId: number,
323
414
  call: OutboundCallFact,
324
415
  ): void {
416
+ const sourceSymbolId = outboundOwnerId(db, repoId, call);
325
417
  const binding = resolvePersistedBinding(db, repoId, call);
326
418
  const external = externalTargetValues(call.externalTarget);
327
419
  const evidence = {
328
420
  ...(call.evidence ?? {}),
329
- serviceBindingResolution: binding.evidence,
421
+ serviceBindingReference: binding.reference,
422
+ serviceBindingResolution: binding.resolution,
330
423
  };
331
424
  stmt.run(
332
- repoId, repoId, call.sourceFile, call.sourceSymbolQualifiedName,
333
- repoId, call.sourceFile, call.sourceLine, call.sourceLine,
425
+ repoId, sourceSymbolId,
334
426
  call.callType, call.method, call.operationPathExpr, call.queryEntity,
335
427
  call.eventNameExpr, call.payloadSummary, call.sourceFile, call.sourceLine,
336
428
  call.callSiteStartOffset, call.callSiteEndOffset, call.confidence,
337
- call.unresolvedReason ?? binding.unresolvedReason,
429
+ call.unresolvedReason,
338
430
  call.localServiceName, call.localServiceLookup,
339
431
  serializedAliasChain(call.aliasChain),
340
432
  JSON.stringify(evidence), external.kind, external.stableId, external.label,
@@ -359,17 +451,67 @@ function externalTargetValues(
359
451
  };
360
452
  }
361
453
 
362
- interface BindingCandidate {
454
+ interface PersistedBinding {
363
455
  id: number;
364
456
  symbolId?: number | null;
365
457
  variableName: string;
366
- alias?: string | null;
367
- aliasExpr?: string | null;
368
- destinationExpr?: string | null;
369
- servicePathExpr?: string | null;
370
458
  sourceFile: string;
371
- sourceLine: number;
372
- helperChainJson?: string | null;
459
+ siteStart: number;
460
+ siteEnd: number;
461
+ ownerResolution: string;
462
+ ownerStartOffset: number | null;
463
+ ownerEndOffset: number | null;
464
+ singleHopHelperReturn: boolean;
465
+ }
466
+
467
+ function outboundOwnerId(
468
+ db: Db,
469
+ repoId: number,
470
+ call: OutboundCallFact,
471
+ ): number | null {
472
+ const candidates = persistedCallOwners(
473
+ db, repoId, call.sourceFile,
474
+ call.callSiteStartOffset, call.callSiteEndOffset,
475
+ );
476
+ const selected = selectCallOwner(
477
+ candidates,
478
+ call.callSiteStartOffset ?? -1,
479
+ call.callSiteEndOffset ?? -1,
480
+ call.callType === 'async_subscribe',
481
+ );
482
+ const resolution = call.evidence?.sourceOwnerResolution;
483
+ if (resolution === 'ownerless_file_scope')
484
+ return ownerlessOutboundOwner(call, selected.status);
485
+ return ownedOutboundOwner(call, resolution, selected);
486
+ }
487
+
488
+ function ownerlessOutboundOwner(
489
+ call: OutboundCallFact,
490
+ status: string,
491
+ ): null {
492
+ if (status !== 'none')
493
+ throw preparedCallSnapshotError(
494
+ 'outbound_owner_mismatch', 'outbound_call', call,
495
+ );
496
+ return null;
497
+ }
498
+
499
+ function ownedOutboundOwner(
500
+ call: OutboundCallFact,
501
+ resolution: unknown,
502
+ selected: OwnerSelection<PersistedOwnerCandidate>,
503
+ ): number {
504
+ if (resolution !== 'owned_exact' || selected.status !== 'resolved'
505
+ || selected.owner?.qualifiedName !== call.sourceSymbolQualifiedName)
506
+ throw preparedCallSnapshotError(
507
+ 'outbound_owner_mismatch', 'outbound_call', call,
508
+ );
509
+ const owner = selected.owner;
510
+ if (!owner)
511
+ throw preparedCallSnapshotError(
512
+ 'outbound_owner_mismatch', 'outbound_call', call,
513
+ );
514
+ return owner.id;
373
515
  }
374
516
 
375
517
  function resolvePersistedBinding(
@@ -378,160 +520,177 @@ function resolvePersistedBinding(
378
520
  call: OutboundCallFact,
379
521
  ): {
380
522
  bindingId: number | null;
381
- unresolvedReason?: string;
382
- evidence: Record<string, unknown>;
523
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>;
524
+ resolution: Record<string, unknown>;
383
525
  } {
384
- if (!call.serviceVariableName)
385
- return { bindingId: null, evidence: { status: 'not_applicable', candidateCount: 0 } };
386
- const candidates = bindingCandidates(db, repoId, call);
387
- const prior = candidates.filter((candidate) => candidate.sourceLine <= call.sourceLine);
388
- const families = new Set(prior.map(bindingSignature));
389
- if (prior.length > 0 && families.size === 1) {
390
- const selected = prior.at(-1);
391
- return {
392
- bindingId: selected?.id ?? null,
393
- evidence: bindingEvidence('selected', prior, selected),
394
- };
395
- }
396
- if (prior.length > 1) {
397
- return {
398
- bindingId: null,
399
- unresolvedReason: 'ambiguous_service_binding_candidates',
400
- evidence: bindingEvidence('ambiguous', prior),
401
- };
402
- }
403
- if (candidates.length > 0) {
404
- return {
405
- bindingId: null,
406
- unresolvedReason: 'service_binding_declared_after_call',
407
- evidence: bindingEvidence('rejected_future_binding', candidates),
408
- };
409
- }
526
+ const reference = call.serviceBindingReference;
527
+ if (!reference)
528
+ throw preparedCallSnapshotError(
529
+ 'binding_reference_missing', 'outbound_call', call,
530
+ );
531
+ if (reference.status !== 'resolved_exact')
532
+ return unresolvedBinding(reference);
533
+ const candidates = exactBindingRows(db, repoId, reference);
534
+ const selected = candidates[0];
535
+ if (candidates.length !== 1 || !selected
536
+ || selected.variableName !== call.serviceVariableName)
537
+ throw preparedCallSnapshotError(
538
+ 'binding_reference_mismatch', 'outbound_call', call,
539
+ );
540
+ assertResolvedBindingProof(repoId, call, selected, reference);
410
541
  return {
411
- bindingId: null,
412
- evidence: bindingEvidence('unrecoverable', []),
542
+ bindingId: selected.id,
543
+ reference,
544
+ resolution: {
545
+ status: 'selected_exact',
546
+ selectedBindingId: selected.id,
547
+ candidateCount: 1,
548
+ },
413
549
  };
414
550
  }
415
551
 
416
- function bindingCandidates(
417
- db: Db,
552
+ function bindingProofCall(
418
553
  repoId: number,
419
554
  call: OutboundCallFact,
420
- ): BindingCandidate[] {
421
- const ownerId = callSymbolId(db, repoId, call);
422
- const rows = db.prepare(`
423
- SELECT id,symbol_id symbolId,variable_name variableName,alias,alias_expr aliasExpr,
424
- destination_expr destinationExpr,service_path_expr servicePathExpr,
425
- source_file sourceFile,source_line sourceLine,helper_chain_json helperChainJson
426
- FROM service_bindings
427
- WHERE repo_id=? AND variable_name=? AND source_file=?
428
- AND (? IS NULL OR symbol_id IS NULL OR symbol_id=?)
429
- ORDER BY source_line,id
430
- `).all(
555
+ bindingId: number,
556
+ ): BindingProofCall {
557
+ if (call.callSiteStartOffset === undefined
558
+ || call.callSiteEndOffset === undefined)
559
+ throw preparedCallSnapshotError(
560
+ 'binding_lexical_proof_invalid', 'outbound_call', call,
561
+ );
562
+ return {
431
563
  repoId,
432
- call.serviceVariableName,
433
- call.sourceFile,
434
- ownerId,
435
- ownerId,
436
- ) as Array<Record<string, unknown>>;
437
- return rows.flatMap((row) => {
438
- if (typeof row.id !== 'number' || typeof row.variableName !== 'string'
439
- || typeof row.sourceFile !== 'string' || typeof row.sourceLine !== 'number')
440
- return [];
441
- return [{
442
- id: row.id,
443
- symbolId: nullableNumber(row.symbolId),
444
- variableName: row.variableName,
445
- alias: nullableString(row.alias),
446
- aliasExpr: nullableString(row.aliasExpr),
447
- destinationExpr: nullableString(row.destinationExpr),
448
- servicePathExpr: nullableString(row.servicePathExpr),
449
- sourceFile: row.sourceFile,
450
- sourceLine: row.sourceLine,
451
- helperChainJson: nullableString(row.helperChainJson),
452
- }];
453
- });
454
- }
455
-
456
- function nullableString(value: unknown): string | null | undefined {
457
- return value === null || typeof value === 'string' ? value : undefined;
458
- }
459
-
460
- function nullableNumber(value: unknown): number | null | undefined {
461
- return value === null || typeof value === 'number' ? value : undefined;
564
+ bindingId,
565
+ variableName: call.serviceVariableName,
566
+ sourceFile: call.sourceFile,
567
+ startOffset: call.callSiteStartOffset,
568
+ endOffset: call.callSiteEndOffset,
569
+ };
462
570
  }
463
571
 
464
- function callSymbolId(
465
- db: Db,
572
+ function bindingProofTarget(
466
573
  repoId: number,
467
- call: OutboundCallFact,
468
- ): number | undefined {
469
- const row = db.prepare(`
470
- SELECT id FROM symbols
471
- WHERE repo_id=? AND source_file=?
472
- AND ((? IS NOT NULL AND qualified_name=?)
473
- OR (start_line<=? AND end_line>=?))
474
- ORDER BY CASE WHEN qualified_name=? THEN 0 ELSE 1 END,
475
- (end_line-start_line),id
476
- LIMIT 1
477
- `).get(
574
+ binding: PersistedBinding,
575
+ ): BindingProofTarget {
576
+ return {
577
+ id: binding.id,
478
578
  repoId,
479
- call.sourceFile,
480
- call.sourceSymbolQualifiedName,
481
- call.sourceSymbolQualifiedName,
482
- call.sourceLine,
483
- call.sourceLine,
484
- call.sourceSymbolQualifiedName,
579
+ symbolId: binding.symbolId ?? null,
580
+ variableName: binding.variableName,
581
+ sourceFile: binding.sourceFile,
582
+ startOffset: binding.siteStart,
583
+ endOffset: binding.siteEnd,
584
+ ownerResolution: binding.ownerResolution,
585
+ ownerStartOffset: binding.ownerStartOffset,
586
+ ownerEndOffset: binding.ownerEndOffset,
587
+ singleHopHelperReturn: binding.singleHopHelperReturn,
588
+ };
589
+ }
590
+
591
+ function assertResolvedBindingProof(
592
+ repoId: number,
593
+ call: OutboundCallFact,
594
+ binding: PersistedBinding,
595
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>,
596
+ ): void {
597
+ const valid = resolvedBindingReferenceProofValid(
598
+ bindingProofCall(repoId, call, binding.id),
599
+ reference,
600
+ bindingProofTarget(repoId, binding),
485
601
  );
486
- return typeof row?.id === 'number' ? row.id : undefined;
602
+ if (!valid)
603
+ throw preparedCallSnapshotError(
604
+ 'binding_lexical_proof_invalid', 'outbound_call', call,
605
+ );
487
606
  }
488
607
 
489
- function bindingEvidence(
490
- status: string,
491
- candidates: BindingCandidate[],
492
- selected?: BindingCandidate,
493
- ): Record<string, unknown> {
494
- const projection = projectBounded(candidates, (left, right) =>
495
- Number(right.id === selected?.id) - Number(left.id === selected?.id)
496
- || left.sourceFile.localeCompare(right.sourceFile)
497
- || left.sourceLine - right.sourceLine
498
- || left.id - right.id);
608
+ function unresolvedBinding(
609
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>,
610
+ ): {
611
+ bindingId: null;
612
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>;
613
+ resolution: Record<string, unknown>;
614
+ } {
499
615
  return {
500
- status,
501
- candidateCount: projection.totalCount,
502
- shownCandidateCount: projection.shownCount,
503
- omittedCandidateCount: projection.omittedCount,
504
- selectedBindingId: selected?.id,
505
- sourceOrderRule: 'binding_source_line_must_not_follow_call',
506
- candidates: projection.items.map((candidate) => ({
507
- bindingId: candidate.id,
508
- symbolId: candidate.symbolId,
509
- variableName: candidate.variableName,
510
- alias: candidate.alias,
511
- aliasExpr: candidate.aliasExpr,
512
- destinationExpr: candidate.destinationExpr,
513
- servicePathExpr: candidate.servicePathExpr,
514
- sourceFile: candidate.sourceFile,
515
- sourceLine: candidate.sourceLine,
516
- helperChain: parseBindingChain(candidate.helperChainJson),
517
- })),
616
+ bindingId: null,
617
+ reference,
618
+ resolution: {
619
+ status: reference.status,
620
+ candidateCount: 0,
621
+ },
518
622
  };
519
623
  }
520
624
 
521
- function bindingSignature(candidate: BindingCandidate): string {
522
- return JSON.stringify([
523
- candidate.alias,
524
- candidate.aliasExpr,
525
- candidate.destinationExpr,
526
- candidate.servicePathExpr,
527
- ]);
625
+ function exactBindingRows(
626
+ db: Db,
627
+ repoId: number,
628
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>,
629
+ ): PersistedBinding[] {
630
+ const rows = db.prepare(`SELECT binding.id,binding.symbol_id symbolId,
631
+ binding.variable_name variableName,binding.source_file sourceFile,
632
+ binding.binding_site_start_offset siteStart,
633
+ binding.binding_site_end_offset siteEnd,
634
+ binding.owner_resolution ownerResolution,
635
+ binding.helper_chain_json helperChainJson,
636
+ owner.start_offset ownerStartOffset,owner.end_offset ownerEndOffset
637
+ FROM service_bindings binding
638
+ LEFT JOIN symbols owner ON owner.id=binding.symbol_id
639
+ WHERE binding.repo_id=? AND binding.source_file=?
640
+ AND binding.variable_name=?
641
+ AND binding.binding_site_start_offset=?
642
+ AND binding.binding_site_end_offset=?`).all(
643
+ repoId,
644
+ reference.bindingSourceFile,
645
+ reference.variableName,
646
+ reference.bindingSiteStartOffset,
647
+ reference.bindingSiteEndOffset,
648
+ );
649
+ return rows.flatMap(persistedBindingRow);
528
650
  }
529
651
 
530
- function parseBindingChain(value: string | null | undefined): unknown {
531
- if (!value) return undefined;
532
- try {
533
- return JSON.parse(value) as unknown;
534
- } catch {
535
- return undefined;
536
- }
652
+ function persistedBindingRow(
653
+ row: Record<string, unknown>,
654
+ ): PersistedBinding[] {
655
+ const ownerStartOffset = nullableOffset(row.ownerStartOffset);
656
+ const ownerEndOffset = nullableOffset(row.ownerEndOffset);
657
+ if (!persistedBindingRequiredValid(row)
658
+ || ownerStartOffset === undefined || ownerEndOffset === undefined) return [];
659
+ return [{
660
+ id: row.id,
661
+ symbolId: row.symbolId === null || typeof row.symbolId === 'number'
662
+ ? row.symbolId : undefined,
663
+ variableName: row.variableName,
664
+ sourceFile: row.sourceFile,
665
+ siteStart: row.siteStart,
666
+ siteEnd: row.siteEnd,
667
+ ownerResolution: row.ownerResolution,
668
+ ownerStartOffset,
669
+ ownerEndOffset,
670
+ singleHopHelperReturn: hasSingleHopHelperReturn(row.helperChainJson),
671
+ }];
672
+ }
673
+
674
+ function nullableOffset(value: unknown): number | null | undefined {
675
+ return value === null || typeof value === 'number' ? value : undefined;
676
+ }
677
+
678
+ function persistedBindingRequiredValid(
679
+ row: Record<string, unknown>,
680
+ ): row is Record<string, unknown> & {
681
+ id: number;
682
+ variableName: string;
683
+ sourceFile: string;
684
+ siteStart: number;
685
+ siteEnd: number;
686
+ ownerResolution: string;
687
+ } {
688
+ return [
689
+ typeof row.id === 'number',
690
+ typeof row.variableName === 'string',
691
+ typeof row.sourceFile === 'string',
692
+ typeof row.siteStart === 'number',
693
+ typeof row.siteEnd === 'number',
694
+ typeof row.ownerResolution === 'string',
695
+ ].every(Boolean);
537
696
  }