@saptools/service-flow 0.1.66 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-TOVX4WYH.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. package/dist/chunk-TOVX4WYH.js.map +0 -1
@@ -1,17 +1,32 @@
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';
5
19
 
6
20
  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
21
  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
22
  for (const r of rows) {
10
- const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName) as { id?: number } | undefined;
23
+ assertImportProvenance(r);
24
+ const callerId = requiredSymbolCallOwnerId(db, repoId, r);
11
25
  const target = resolveSymbolCallTarget(db, repoId, r);
26
+ const cardinality = resolutionCardinality(target);
12
27
  insertStmt.run(
13
28
  repoId,
14
- caller?.id,
29
+ callerId,
15
30
  target.id,
16
31
  r.calleeExpression,
17
32
  r.importSource,
@@ -26,6 +41,10 @@ export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]
26
41
  ...r.evidence,
27
42
  candidateStrategy: target.strategy,
28
43
  candidateCount: target.candidateCount,
44
+ eligibleCandidateCount: cardinality.eligibleCandidateCount,
45
+ selectedCandidateCount: cardinality.selectedCandidateCount,
46
+ candidateSetComplete: cardinality.candidateSetComplete,
47
+ unresolvedReason: target.reason,
29
48
  resolvedModulePath: target.resolvedModulePath,
30
49
  }),
31
50
  target.reason,
@@ -33,6 +52,40 @@ export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]
33
52
  }
34
53
  }
35
54
 
55
+ function assertImportProvenance(call: SymbolCallFact): void {
56
+ if (call.importSource === undefined || call.importSource.startsWith('.'))
57
+ return;
58
+ const direct = directPackageProvenanceValid(call);
59
+ const derived = derivedPackageProvenanceValid(call);
60
+ if (!direct && !derived)
61
+ throw new Error(
62
+ 'invalid_prepared_repository_snapshot:package_import_provenance_missing',
63
+ );
64
+ }
65
+
66
+ function directPackageProvenanceValid(call: SymbolCallFact): boolean {
67
+ const binding = parsePackageImportReference(call.evidence.importBinding);
68
+ return Boolean(binding
69
+ && call.evidence.relation === 'package_import'
70
+ && call.evidence.derivedImportBinding === undefined
71
+ && call.importSource === binding.rawModuleSpecifier
72
+ && call.evidence.targetName === binding.requestedPublicName);
73
+ }
74
+
75
+ function derivedPackageProvenanceValid(call: SymbolCallFact): boolean {
76
+ const binding = parsePackageImportReference(
77
+ call.evidence.derivedImportBinding,
78
+ );
79
+ if (!binding || typeof binding.referencedMemberName !== 'string')
80
+ return false;
81
+ const expected = typeof call.evidence.proxyVariableName === 'string'
82
+ ? binding.referencedMemberName : binding.requestedPublicName;
83
+ return call.evidence.relation === 'package_import_derived_member'
84
+ && call.evidence.importBinding === undefined
85
+ && call.importSource === binding.rawModuleSpecifier
86
+ && call.evidence.targetName === expected;
87
+ }
88
+
36
89
  interface SymbolTargetRow {
37
90
  id: number;
38
91
  kind?: string;
@@ -46,38 +99,114 @@ interface SymbolCallResolution {
46
99
  reason: string | null;
47
100
  strategy: string;
48
101
  candidateCount: number;
102
+ eligibleCandidateCount: number;
103
+ candidateSetComplete: boolean;
49
104
  resolvedModulePath?: string;
50
105
  }
51
106
 
52
- const stripExt = (value: string): string => value.replace(/\.(ts|tsx|js|jsx|cds)$/, '');
107
+ interface PersistedOwnerCandidate extends OwnerCandidate {
108
+ id: number;
109
+ }
110
+
111
+ function persistedCallOwners(
112
+ db: Db,
113
+ repoId: number,
114
+ sourceFile: string,
115
+ start: number | undefined,
116
+ end: number | undefined,
117
+ ): PersistedOwnerCandidate[] {
118
+ if (start === undefined || end === undefined) return [];
119
+ const rows = db.prepare(`SELECT id,kind,qualified_name qualifiedName,
120
+ start_offset startOffset,end_offset endOffset FROM symbols
121
+ WHERE repo_id=? AND source_file=? AND start_offset<=? AND end_offset>=?`)
122
+ .all(repoId, sourceFile, start, end);
123
+ return rows.flatMap(persistedOwnerCandidate);
124
+ }
125
+
126
+ function persistedOwnerCandidate(
127
+ row: Record<string, unknown>,
128
+ ): PersistedOwnerCandidate[] {
129
+ if (typeof row.id !== 'number' || typeof row.kind !== 'string'
130
+ || typeof row.qualifiedName !== 'string'
131
+ || typeof row.startOffset !== 'number'
132
+ || typeof row.endOffset !== 'number') return [];
133
+ return [{
134
+ id: row.id,
135
+ kind: row.kind,
136
+ qualifiedName: row.qualifiedName,
137
+ startOffset: row.startOffset,
138
+ endOffset: row.endOffset,
139
+ }];
140
+ }
141
+
142
+ function requiredSymbolCallOwnerId(
143
+ db: Db,
144
+ repoId: number,
145
+ call: SymbolCallFact,
146
+ ): number {
147
+ const candidates = persistedCallOwners(
148
+ db, repoId, call.sourceFile,
149
+ call.callSiteStartOffset, call.callSiteEndOffset,
150
+ );
151
+ const selected = selectCallOwner(
152
+ candidates,
153
+ call.callSiteStartOffset ?? -1,
154
+ call.callSiteEndOffset ?? -1,
155
+ call.callRole === 'event_subscribe_handler',
156
+ );
157
+ if (selected.status !== 'resolved'
158
+ || selected.owner?.qualifiedName !== call.callerQualifiedName)
159
+ throw new Error('invalid_prepared_repository_snapshot:symbol_call_owner_mismatch');
160
+ return selected.owner.id;
161
+ }
162
+
163
+ function resolutionCardinality(
164
+ target: SymbolCallResolution,
165
+ ): {
166
+ eligibleCandidateCount: number;
167
+ selectedCandidateCount: 0 | 1;
168
+ candidateSetComplete: boolean;
169
+ } {
170
+ return {
171
+ eligibleCandidateCount: target.eligibleCandidateCount,
172
+ selectedCandidateCount: target.status === 'resolved' ? 1 : 0,
173
+ candidateSetComplete: target.candidateSetComplete,
174
+ };
175
+ }
53
176
 
54
177
  function symbolTargetRows(rows: Array<Record<string, unknown>>): SymbolTargetRow[] {
55
178
  return rows.flatMap((row) => typeof row.id === 'number' ? [{
56
179
  id: row.id,
57
180
  kind: typeof row.kind === 'string' ? row.kind : undefined,
58
- sourceFile: nullableString(row.sourceFile),
59
- evidenceJson: nullableString(row.evidenceJson),
181
+ sourceFile: typeof row.sourceFile === 'string' ? row.sourceFile : null,
182
+ evidenceJson: typeof row.evidenceJson === 'string'
183
+ ? row.evidenceJson : null,
60
184
  }] : []);
61
185
  }
62
186
 
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`]);
187
+ function record(value: unknown): Record<string, unknown> | undefined {
188
+ return value && typeof value === 'object' && !Array.isArray(value)
189
+ ? value as Record<string, unknown> : undefined;
67
190
  }
68
191
 
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)));
192
+ function executableTarget(row: SymbolTargetRow): boolean {
193
+ if (!row.evidenceJson || !row.kind
194
+ || !['function', 'method', 'object_method', 'callback'].includes(row.kind))
195
+ return false;
196
+ try {
197
+ const evidence = record(JSON.parse(row.evidenceJson) as unknown);
198
+ const body = record(evidence?.executableBodyEligibility);
199
+ return body?.eligible === true && body.reason === 'body_present';
200
+ } catch {
201
+ return false;
202
+ }
74
203
  }
75
204
 
76
205
  function resolvedSymbol(
77
206
  row: SymbolTargetRow,
78
207
  strategy: string,
79
208
  candidateCount: number,
80
- moduleScoped = false,
209
+ eligibleCandidateCount = 1,
81
210
  ): SymbolCallResolution {
82
211
  return {
83
212
  id: row.id,
@@ -85,211 +214,169 @@ function resolvedSymbol(
85
214
  reason: null,
86
215
  strategy,
87
216
  candidateCount,
88
- resolvedModulePath: moduleScoped && row.sourceFile
89
- ? stripExt(row.sourceFile)
90
- : undefined,
217
+ eligibleCandidateCount,
218
+ candidateSetComplete: true,
91
219
  };
92
220
  }
93
221
 
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
- };
222
+ function unresolvedSymbol(
223
+ strategy: string,
224
+ reason: string,
225
+ candidateCount: number,
226
+ eligibleCandidateCount = 0,
227
+ candidateSetComplete = true,
228
+ ): SymbolCallResolution {
165
229
  return {
166
230
  id: null,
167
231
  status: 'unresolved',
168
- reason: 'No namespace member target matched the imported module',
169
- strategy: 'relative_import_namespace_member',
170
- candidateCount: 0,
232
+ reason,
233
+ strategy,
234
+ candidateCount,
235
+ eligibleCandidateCount,
236
+ candidateSetComplete,
171
237
  };
172
238
  }
173
239
 
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);
240
+ function ambiguousSymbol(
241
+ strategy: string,
242
+ reason: string,
243
+ candidateCount: number,
244
+ eligibleCandidateCount: number,
245
+ ): SymbolCallResolution {
194
246
  return {
195
247
  id: null,
196
248
  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,
249
+ reason,
250
+ strategy,
251
+ candidateCount,
252
+ eligibleCandidateCount,
253
+ candidateSetComplete: true,
200
254
  };
201
255
  }
202
256
 
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');
257
+ function exportedSymbolRows(db: Db, repoId: number, r: SymbolCallFact): SymbolTargetRow[] {
258
+ return symbolTargetRows(db.prepare(`SELECT id,kind,
259
+ source_file sourceFile,evidence_json evidenceJson FROM symbols
260
+ WHERE repo_id=? AND source_file<>? AND exported=1
261
+ AND (exported_name=? OR name=? OR qualified_name=?)
262
+ ORDER BY id`).all(
263
+ repoId, r.sourceFile,
264
+ r.calleeLocalName, r.calleeLocalName, r.calleeLocalName,
265
+ ));
207
266
  }
208
267
 
209
- function exportedResolution(
268
+ function isRelativeImportedSymbolCall(r: SymbolCallFact): boolean {
269
+ return Boolean(r.importSource?.startsWith('.'));
270
+ }
271
+
272
+ function eligibleSymbolResolution(
210
273
  rows: SymbolTargetRow[],
211
- r: SymbolCallFact,
212
- relation: unknown,
274
+ strategy: string,
275
+ ambiguousReason: string,
213
276
  ): 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,
277
+ if (rows.length === 0) return undefined;
278
+ const eligible = rows.filter(executableTarget);
279
+ if (eligible.length === 1 && eligible[0])
280
+ return resolvedSymbol(eligible[0], strategy, rows.length, 1);
281
+ if (eligible.length > 1) return ambiguousSymbol(
282
+ strategy, ambiguousReason, rows.length, eligible.length,
283
+ );
284
+ return unresolvedSymbol(
285
+ strategy, 'symbol_target_has_no_executable_body', rows.length,
221
286
  );
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
287
  }
234
288
 
235
- function accessorResolution(
289
+ function sameFileResolution(
236
290
  db: Db,
237
291
  repoId: number,
238
292
  r: SymbolCallFact,
239
293
  relation: unknown,
240
294
  ): 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,
295
+ if (isRelativeImportedSymbolCall(r) || relation === 'package_import')
296
+ return undefined;
297
+ if (relation === 'indexed_local_symbol_unproven')
298
+ return unresolvedSymbol(
299
+ 'exact_symbol_match', 'no_local_symbol_target', 0,
300
+ );
301
+ if (relation === 'indexed_local_symbol'
302
+ && r.evidence.localTargetIdentity !== undefined)
303
+ return exactLocalSymbolResolution(db, repoId, r);
304
+ const rows = symbolTargetRows(db.prepare(`SELECT id,kind,
305
+ source_file sourceFile,evidence_json evidenceJson FROM symbols
306
+ WHERE repo_id=? AND source_file=?
307
+ AND (name=? OR qualified_name=?) ORDER BY id`).all(
308
+ repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName,
309
+ ));
310
+ return eligibleSymbolResolution(
311
+ rows, 'same_file_exact', 'multiple_same_file_symbol_targets',
312
+ );
313
+ }
314
+
315
+ function exactLocalSymbolResolution(
316
+ db: Db,
317
+ repoId: number,
318
+ call: SymbolCallFact,
319
+ ): SymbolCallResolution {
320
+ const target = record(call.evidence.localTargetIdentity);
321
+ if (!target
322
+ || typeof target.sourceFile !== 'string'
323
+ || typeof target.qualifiedName !== 'string'
324
+ || typeof target.startOffset !== 'number'
325
+ || typeof target.endOffset !== 'number')
326
+ return unresolvedSymbol(
327
+ 'exact_symbol_match', 'no_local_symbol_target', 0,
328
+ );
329
+ const rows = symbolTargetRows(db.prepare(`SELECT id,kind,
330
+ source_file sourceFile,evidence_json evidenceJson FROM symbols
331
+ WHERE repo_id=? AND source_file=? AND qualified_name=?
332
+ AND start_offset=? AND end_offset=? ORDER BY id`).all(
333
+ repoId, target.sourceFile, target.qualifiedName,
334
+ target.startOffset, target.endOffset,
335
+ ));
336
+ return eligibleSymbolResolution(
337
+ rows, 'same_file_exact', 'multiple_same_file_symbol_targets',
338
+ ) ?? unresolvedSymbol(
339
+ 'exact_symbol_match', 'no_local_symbol_target', 0,
340
+ );
341
+ }
342
+
343
+ function exportedResolution(
344
+ rows: SymbolTargetRow[],
345
+ ): SymbolCallResolution | undefined {
346
+ return eligibleSymbolResolution(
347
+ rows, 'exported_exact', 'multiple_exported_symbol_targets',
250
348
  );
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(
349
+ }
350
+
351
+ export function resolveSymbolCallTarget(
263
352
  db: Db,
264
353
  repoId: number,
265
354
  r: SymbolCallFact,
266
355
  ): SymbolCallResolution {
267
356
  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);
357
+ const relative = resolveRelativeSymbolCall(db, repoId, r, relation);
358
+ if (relative) return relative;
359
+ const early = sameFileResolution(db, repoId, r, relation);
271
360
  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);
361
+ if (relation === 'package_import_derived_member') return unresolvedSymbol(
362
+ 'package_import_derived_member_unsupported',
363
+ 'package_derived_member_provenance_insufficient',
364
+ 0,
365
+ );
366
+ if (relation === 'package_import') return unresolvedSymbol(
367
+ 'package_import_pending',
368
+ 'package_resolution_pending',
369
+ 0,
370
+ 0,
371
+ false,
372
+ );
373
+ const matched = exportedResolution(exportedSymbolRows(db, repoId, r));
276
374
  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
- };
375
+ return unresolvedSymbol(
376
+ 'exact_symbol_match',
377
+ 'no_local_symbol_target',
378
+ 0,
379
+ );
293
380
  }
294
381
 
295
382
  export function insertCalls(
@@ -308,12 +395,7 @@ function outboundCallInsertStatement(db: Db): Statement {
308
395
  call_site_end_offset,confidence,unresolved_reason,local_service_name,
309
396
  local_service_lookup,alias_chain_json,evidence_json,external_target_kind,
310
397
  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
- )`);
398
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
317
399
  }
318
400
 
319
401
  function insertOutboundCall(
@@ -322,19 +404,20 @@ function insertOutboundCall(
322
404
  repoId: number,
323
405
  call: OutboundCallFact,
324
406
  ): void {
407
+ const sourceSymbolId = outboundOwnerId(db, repoId, call);
325
408
  const binding = resolvePersistedBinding(db, repoId, call);
326
409
  const external = externalTargetValues(call.externalTarget);
327
410
  const evidence = {
328
411
  ...(call.evidence ?? {}),
329
- serviceBindingResolution: binding.evidence,
412
+ serviceBindingReference: binding.reference,
413
+ serviceBindingResolution: binding.resolution,
330
414
  };
331
415
  stmt.run(
332
- repoId, repoId, call.sourceFile, call.sourceSymbolQualifiedName,
333
- repoId, call.sourceFile, call.sourceLine, call.sourceLine,
416
+ repoId, sourceSymbolId,
334
417
  call.callType, call.method, call.operationPathExpr, call.queryEntity,
335
418
  call.eventNameExpr, call.payloadSummary, call.sourceFile, call.sourceLine,
336
419
  call.callSiteStartOffset, call.callSiteEndOffset, call.confidence,
337
- call.unresolvedReason ?? binding.unresolvedReason,
420
+ call.unresolvedReason,
338
421
  call.localServiceName, call.localServiceLookup,
339
422
  serializedAliasChain(call.aliasChain),
340
423
  JSON.stringify(evidence), external.kind, external.stableId, external.label,
@@ -359,17 +442,57 @@ function externalTargetValues(
359
442
  };
360
443
  }
361
444
 
362
- interface BindingCandidate {
445
+ interface PersistedBinding {
363
446
  id: number;
364
447
  symbolId?: number | null;
365
448
  variableName: string;
366
- alias?: string | null;
367
- aliasExpr?: string | null;
368
- destinationExpr?: string | null;
369
- servicePathExpr?: string | null;
370
449
  sourceFile: string;
371
- sourceLine: number;
372
- helperChainJson?: string | null;
450
+ siteStart: number;
451
+ siteEnd: number;
452
+ ownerResolution: string;
453
+ ownerStartOffset: number | null;
454
+ ownerEndOffset: number | null;
455
+ }
456
+
457
+ function outboundOwnerId(
458
+ db: Db,
459
+ repoId: number,
460
+ call: OutboundCallFact,
461
+ ): number | null {
462
+ const candidates = persistedCallOwners(
463
+ db, repoId, call.sourceFile,
464
+ call.callSiteStartOffset, call.callSiteEndOffset,
465
+ );
466
+ const selected = selectCallOwner(
467
+ candidates,
468
+ call.callSiteStartOffset ?? -1,
469
+ call.callSiteEndOffset ?? -1,
470
+ call.callType === 'async_subscribe',
471
+ );
472
+ const resolution = call.evidence?.sourceOwnerResolution;
473
+ if (resolution === 'ownerless_file_scope')
474
+ return ownerlessOutboundOwner(selected.status);
475
+ return ownedOutboundOwner(call, resolution, selected);
476
+ }
477
+
478
+ function ownerlessOutboundOwner(status: string): null {
479
+ if (status !== 'none')
480
+ throw new Error('invalid_prepared_repository_snapshot:outbound_owner_mismatch');
481
+ return null;
482
+ }
483
+
484
+ function ownedOutboundOwner(
485
+ call: OutboundCallFact,
486
+ resolution: unknown,
487
+ selected: OwnerSelection<PersistedOwnerCandidate>,
488
+ ): number {
489
+ if (resolution !== 'owned_exact' || selected.status !== 'resolved'
490
+ || selected.owner?.qualifiedName !== call.sourceSymbolQualifiedName)
491
+ throw new Error('invalid_prepared_repository_snapshot:outbound_owner_mismatch');
492
+ const owner = selected.owner;
493
+ if (!owner)
494
+ throw new Error('invalid_prepared_repository_snapshot:outbound_owner_mismatch');
495
+ return owner.id;
373
496
  }
374
497
 
375
498
  function resolvePersistedBinding(
@@ -378,160 +501,170 @@ function resolvePersistedBinding(
378
501
  call: OutboundCallFact,
379
502
  ): {
380
503
  bindingId: number | null;
381
- unresolvedReason?: string;
382
- evidence: Record<string, unknown>;
504
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>;
505
+ resolution: Record<string, unknown>;
383
506
  } {
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
- }
507
+ const reference = call.serviceBindingReference;
508
+ if (!reference)
509
+ throw new Error('invalid_prepared_repository_snapshot:binding_reference_missing');
510
+ if (reference.status !== 'resolved_exact')
511
+ return unresolvedBinding(reference);
512
+ const candidates = exactBindingRows(db, repoId, reference);
513
+ const selected = candidates[0];
514
+ if (candidates.length !== 1 || !selected
515
+ || selected.variableName !== call.serviceVariableName)
516
+ throw new Error('invalid_prepared_repository_snapshot:binding_reference_mismatch');
517
+ assertResolvedBindingProof(repoId, call, selected, reference);
410
518
  return {
411
- bindingId: null,
412
- evidence: bindingEvidence('unrecoverable', []),
519
+ bindingId: selected.id,
520
+ reference,
521
+ resolution: {
522
+ status: 'selected_exact',
523
+ selectedBindingId: selected.id,
524
+ candidateCount: 1,
525
+ },
413
526
  };
414
527
  }
415
528
 
416
- function bindingCandidates(
417
- db: Db,
529
+ function bindingProofCall(
418
530
  repoId: number,
419
531
  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(
532
+ bindingId: number,
533
+ ): BindingProofCall {
534
+ if (call.callSiteStartOffset === undefined
535
+ || call.callSiteEndOffset === undefined)
536
+ throw new Error(
537
+ 'invalid_prepared_repository_snapshot:binding_lexical_proof_invalid',
538
+ );
539
+ return {
431
540
  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;
541
+ bindingId,
542
+ variableName: call.serviceVariableName,
543
+ sourceFile: call.sourceFile,
544
+ startOffset: call.callSiteStartOffset,
545
+ endOffset: call.callSiteEndOffset,
546
+ };
462
547
  }
463
548
 
464
- function callSymbolId(
465
- db: Db,
549
+ function bindingProofTarget(
466
550
  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(
551
+ binding: PersistedBinding,
552
+ ): BindingProofTarget {
553
+ return {
554
+ id: binding.id,
478
555
  repoId,
479
- call.sourceFile,
480
- call.sourceSymbolQualifiedName,
481
- call.sourceSymbolQualifiedName,
482
- call.sourceLine,
483
- call.sourceLine,
484
- call.sourceSymbolQualifiedName,
556
+ symbolId: binding.symbolId ?? null,
557
+ variableName: binding.variableName,
558
+ sourceFile: binding.sourceFile,
559
+ startOffset: binding.siteStart,
560
+ endOffset: binding.siteEnd,
561
+ ownerResolution: binding.ownerResolution,
562
+ ownerStartOffset: binding.ownerStartOffset,
563
+ ownerEndOffset: binding.ownerEndOffset,
564
+ };
565
+ }
566
+
567
+ function assertResolvedBindingProof(
568
+ repoId: number,
569
+ call: OutboundCallFact,
570
+ binding: PersistedBinding,
571
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>,
572
+ ): void {
573
+ const valid = resolvedBindingReferenceProofValid(
574
+ bindingProofCall(repoId, call, binding.id),
575
+ reference,
576
+ bindingProofTarget(repoId, binding),
485
577
  );
486
- return typeof row?.id === 'number' ? row.id : undefined;
487
- }
488
-
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);
578
+ if (!valid)
579
+ throw new Error(
580
+ 'invalid_prepared_repository_snapshot:binding_lexical_proof_invalid',
581
+ );
582
+ }
583
+
584
+ function unresolvedBinding(
585
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>,
586
+ ): {
587
+ bindingId: null;
588
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>;
589
+ resolution: Record<string, unknown>;
590
+ } {
499
591
  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
- })),
592
+ bindingId: null,
593
+ reference,
594
+ resolution: {
595
+ status: reference.status,
596
+ candidateCount: 0,
597
+ },
518
598
  };
519
599
  }
520
600
 
521
- function bindingSignature(candidate: BindingCandidate): string {
522
- return JSON.stringify([
523
- candidate.alias,
524
- candidate.aliasExpr,
525
- candidate.destinationExpr,
526
- candidate.servicePathExpr,
527
- ]);
601
+ function exactBindingRows(
602
+ db: Db,
603
+ repoId: number,
604
+ reference: NonNullable<OutboundCallFact['serviceBindingReference']>,
605
+ ): PersistedBinding[] {
606
+ const rows = db.prepare(`SELECT binding.id,binding.symbol_id symbolId,
607
+ binding.variable_name variableName,binding.source_file sourceFile,
608
+ binding.binding_site_start_offset siteStart,
609
+ binding.binding_site_end_offset siteEnd,
610
+ binding.owner_resolution ownerResolution,
611
+ owner.start_offset ownerStartOffset,owner.end_offset ownerEndOffset
612
+ FROM service_bindings binding
613
+ LEFT JOIN symbols owner ON owner.id=binding.symbol_id
614
+ WHERE binding.repo_id=? AND binding.source_file=?
615
+ AND binding.variable_name=?
616
+ AND binding.binding_site_start_offset=?
617
+ AND binding.binding_site_end_offset=?`).all(
618
+ repoId,
619
+ reference.bindingSourceFile,
620
+ reference.variableName,
621
+ reference.bindingSiteStartOffset,
622
+ reference.bindingSiteEndOffset,
623
+ );
624
+ return rows.flatMap(persistedBindingRow);
528
625
  }
529
626
 
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
- }
627
+ function persistedBindingRow(
628
+ row: Record<string, unknown>,
629
+ ): PersistedBinding[] {
630
+ const ownerStartOffset = nullableOffset(row.ownerStartOffset);
631
+ const ownerEndOffset = nullableOffset(row.ownerEndOffset);
632
+ if (!persistedBindingRequiredValid(row)
633
+ || ownerStartOffset === undefined || ownerEndOffset === undefined) return [];
634
+ return [{
635
+ id: row.id,
636
+ symbolId: row.symbolId === null || typeof row.symbolId === 'number'
637
+ ? row.symbolId : undefined,
638
+ variableName: row.variableName,
639
+ sourceFile: row.sourceFile,
640
+ siteStart: row.siteStart,
641
+ siteEnd: row.siteEnd,
642
+ ownerResolution: row.ownerResolution,
643
+ ownerStartOffset,
644
+ ownerEndOffset,
645
+ }];
646
+ }
647
+
648
+ function nullableOffset(value: unknown): number | null | undefined {
649
+ return value === null || typeof value === 'number' ? value : undefined;
650
+ }
651
+
652
+ function persistedBindingRequiredValid(
653
+ row: Record<string, unknown>,
654
+ ): row is Record<string, unknown> & {
655
+ id: number;
656
+ variableName: string;
657
+ sourceFile: string;
658
+ siteStart: number;
659
+ siteEnd: number;
660
+ ownerResolution: string;
661
+ } {
662
+ return [
663
+ typeof row.id === 'number',
664
+ typeof row.variableName === 'string',
665
+ typeof row.sourceFile === 'string',
666
+ typeof row.siteStart === 'number',
667
+ typeof row.siteEnd === 'number',
668
+ typeof row.ownerResolution === 'string',
669
+ ].every(Boolean);
537
670
  }