@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
@@ -1,4 +1,16 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import type {
3
+ PackagePublicScope,
4
+ PackagePublicSurfaceFact,
5
+ PublicSurfaceTarget,
6
+ } from '../parsers/003-package-public-surface.js';
7
+ import type {
8
+ SymbolImportReference,
9
+ } from '../parsers/002-symbol-import-bindings.js';
10
+ import {
11
+ parsePackageImportReference,
12
+ parsePackagePublicSurfaceFact,
13
+ } from '../parsers/012-package-fact-contract.js';
2
14
 
3
15
  export interface PackageSymbolLinkSummary {
4
16
  resolved: number;
@@ -6,189 +18,409 @@ export interface PackageSymbolLinkSummary {
6
18
  unresolved: number;
7
19
  }
8
20
 
9
- interface RepoExports {
10
- publicName: Map<string, number[]>;
11
- qualified: Map<string, number[]>;
12
- fileById: Map<number, string>;
13
- }
14
-
15
21
  interface PackageCallRow {
16
22
  id: number;
17
23
  callerRepoId: number;
18
- calleeExpression: string;
19
- importSource: string;
24
+ binding: SymbolImportReference;
20
25
  evidence: Record<string, unknown>;
21
26
  }
22
27
 
23
- interface PackageCallResolution {
28
+ interface PackageRepository {
29
+ id: number;
30
+ surface: PackagePublicSurfaceFact;
31
+ }
32
+
33
+ export interface PackageCallResolution {
24
34
  id: number | null;
25
35
  status: 'resolved' | 'ambiguous' | 'unresolved';
26
36
  reason: string | null;
27
- strategy: 'package_import_workspace_resolved' | 'package_import_ambiguous' | 'package_import_unresolved';
37
+ strategy: string;
28
38
  candidateCount: number;
39
+ eligibleCandidateCount: number;
40
+ selectedCandidateCount: 0 | 1;
41
+ candidateSetComplete: boolean;
29
42
  resolvedModulePath?: string;
43
+ targetRepoId?: number;
44
+ repositoryCandidateIds: number[];
45
+ publicSurface?: {
46
+ total: number;
47
+ shown: number;
48
+ omitted: number;
49
+ };
30
50
  }
31
51
 
32
- const unresolvedRepositoryReason = 'Package import target resolution requires a post-publication workspace pass';
33
- const unresolvedSymbolReason = 'Sibling package indexed but no matching exported symbol; the target may be a re-export, barrel, type-only export, or unindexed Receiver.member';
34
- const ambiguousSymbolReason = 'Multiple exported sibling-package symbol targets matched exactly';
35
- const stripExt = (value: string): string => value.replace(/\.(ts|tsx|js|jsx|cds)$/, '');
52
+ export interface PackageResolutionInput {
53
+ callId: number;
54
+ binding: SymbolImportReference;
55
+ }
36
56
 
37
- function push(map: Map<string, number[]>, key: string, id: number): void {
38
- const existing = map.get(key);
39
- if (existing) existing.push(id);
40
- else map.set(key, [id]);
57
+ export interface PackageResolutionResult extends PackageCallResolution {
58
+ callId: number;
41
59
  }
42
60
 
43
- function nullableString(value: unknown): string | undefined {
44
- return typeof value === 'string' ? value : undefined;
61
+ const REPOSITORY_REFERENCE_CAP = 5;
62
+
63
+ function record(value: unknown): Record<string, unknown> | undefined {
64
+ return value && typeof value === 'object' && !Array.isArray(value)
65
+ ? value as Record<string, unknown> : undefined;
45
66
  }
46
67
 
47
- function evidenceObject(value: unknown): Record<string, unknown> {
48
- if (typeof value !== 'string' || value.length === 0) return {};
68
+ function parseRecord(value: unknown): Record<string, unknown> | undefined {
69
+ if (typeof value !== 'string') return undefined;
49
70
  try {
50
- const parsed = JSON.parse(value) as unknown;
51
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
52
- ? parsed as Record<string, unknown>
53
- : {};
71
+ return record(JSON.parse(value) as unknown);
54
72
  } catch {
55
- return {};
73
+ return undefined;
56
74
  }
57
75
  }
58
76
 
59
- function repoByPackageName(db: Db, workspaceId: number): Map<string, number | null> {
60
- const result = new Map<string, number | null>();
61
- const rows = db.prepare(`SELECT id,package_name packageName FROM repositories
62
- WHERE workspace_id=? AND package_name IS NOT NULL ORDER BY package_name,id`).all(workspaceId);
77
+ function importBinding(
78
+ evidence: Record<string, unknown>,
79
+ ): SymbolImportReference | undefined {
80
+ return parsePackageImportReference(evidence.importBinding);
81
+ }
82
+
83
+ function packageCallRows(db: Db, workspaceId: number): PackageCallRow[] {
84
+ const rows = db.prepare(`SELECT sc.id,sc.repo_id callerRepoId,
85
+ sc.evidence_json evidenceJson FROM symbol_calls sc
86
+ JOIN repositories r ON r.id=sc.repo_id
87
+ WHERE r.workspace_id=?
88
+ AND json_extract(sc.evidence_json,'$.importBinding.moduleKind')='package'
89
+ ORDER BY sc.id`).all(workspaceId);
90
+ return rows.flatMap((row) => {
91
+ const evidence = parseRecord(row.evidenceJson);
92
+ const binding = evidence ? importBinding(evidence) : undefined;
93
+ return typeof row.id === 'number' && typeof row.callerRepoId === 'number'
94
+ && evidence && binding
95
+ ? [{ id: row.id, callerRepoId: row.callerRepoId, evidence, binding }]
96
+ : [];
97
+ });
98
+ }
99
+
100
+ function parseSurface(
101
+ value: unknown,
102
+ packageName: string,
103
+ ): PackagePublicSurfaceFact | undefined {
104
+ if (typeof value !== 'string') return undefined;
105
+ try {
106
+ return parsePackagePublicSurfaceFact(
107
+ JSON.parse(value) as unknown,
108
+ packageName,
109
+ );
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ function repositoriesByPackage(
116
+ db: Db,
117
+ workspaceId: number,
118
+ ): Map<string, PackageRepository[]> {
119
+ const rows = db.prepare(`SELECT id,package_name packageName,
120
+ package_public_surface_json surfaceJson FROM repositories
121
+ WHERE workspace_id=? AND package_name IS NOT NULL
122
+ ORDER BY package_name COLLATE BINARY,id`).all(workspaceId);
123
+ const result = new Map<string, PackageRepository[]>();
63
124
  for (const row of rows) {
64
- const packageName = nullableString(row.packageName);
65
- if (!packageName || typeof row.id !== 'number') continue;
66
- result.set(packageName, result.has(packageName) ? null : row.id);
125
+ if (typeof row.id !== 'number' || typeof row.packageName !== 'string')
126
+ continue;
127
+ const surface = parseSurface(row.surfaceJson, row.packageName);
128
+ if (!surface) continue;
129
+ const repositories = result.get(row.packageName) ?? [];
130
+ repositories.push({ id: row.id, surface });
131
+ result.set(row.packageName, repositories);
67
132
  }
68
133
  return result;
69
134
  }
70
135
 
71
- function packagePrefix(importSource: string): string {
72
- const parts = importSource.split('/');
73
- if (importSource.startsWith('@')) return parts.length >= 2 ? parts.slice(0, 2).join('/') : importSource;
74
- return parts[0] ?? importSource;
136
+ function unresolved(
137
+ reason: string,
138
+ options: Partial<PackageCallResolution> = {},
139
+ ): PackageCallResolution {
140
+ return {
141
+ id: null,
142
+ status: 'unresolved',
143
+ reason,
144
+ strategy: 'package_public_surface_unresolved',
145
+ candidateCount: 0,
146
+ eligibleCandidateCount: 0,
147
+ selectedCandidateCount: 0,
148
+ candidateSetComplete: true,
149
+ repositoryCandidateIds: [],
150
+ ...options,
151
+ };
75
152
  }
76
153
 
77
- function packageRepoId(repos: Map<string, number | null>, importSource: string): number | null {
78
- const packageName = repos.has(importSource) ? importSource : packagePrefix(importSource);
79
- return repos.get(packageName) ?? null;
154
+ function repositoryScopeResolution(
155
+ call: PackageCallRow,
156
+ repositories: Map<string, PackageRepository[]>,
157
+ ): PackageRepository | PackageCallResolution {
158
+ const matches = repositories.get(call.binding.requestedPackageName ?? '')
159
+ ?? [];
160
+ if (matches.length === 1 && matches[0]) return matches[0];
161
+ const ids = matches.map((item) => item.id);
162
+ if (matches.length > 1) return unresolved(
163
+ 'package_repository_scope_ambiguous',
164
+ {
165
+ candidateSetComplete: false,
166
+ candidateCount: matches.length,
167
+ repositoryCandidateIds: ids,
168
+ },
169
+ );
170
+ return unresolved('package_repository_not_indexed');
80
171
  }
81
172
 
82
- function emptyRepoExports(): RepoExports {
83
- return { publicName: new Map(), qualified: new Map(), fileById: new Map() };
173
+ function publicScope(
174
+ surface: PackagePublicSurfaceFact,
175
+ binding: SymbolImportReference,
176
+ ): PackagePublicScope | PackageCallResolution {
177
+ const surfaceCounts = {
178
+ total: surface.total,
179
+ shown: surface.shown,
180
+ omitted: surface.omitted,
181
+ };
182
+ if (surface.status !== 'complete') return unresolved(
183
+ surface.status === 'unsupported'
184
+ ? 'package_public_surface_unsupported'
185
+ : 'public_surface_evidence_incomplete',
186
+ { candidateSetComplete: false, publicSurface: surfaceCounts },
187
+ );
188
+ if (binding.typeOnly) return unresolved(
189
+ 'package_binding_type_only',
190
+ { publicSurface: surfaceCounts },
191
+ );
192
+ const matches = surface.scopes.filter((scope) =>
193
+ scope.entry === binding.requestedModuleSubpath
194
+ && scope.publicName === binding.requestedPublicName);
195
+ if (matches.length === 1 && matches[0]) return matches[0];
196
+ if (matches.length > 1) return unresolved(
197
+ 'package_public_scope_duplicate',
198
+ {
199
+ candidateSetComplete: false,
200
+ candidateCount: matches.reduce(
201
+ (sum, scope) => sum + scope.candidateCount, 0,
202
+ ),
203
+ publicSurface: surfaceCounts,
204
+ },
205
+ );
206
+ return unresolved(
207
+ surface.omitted > 0
208
+ ? 'public_surface_evidence_incomplete'
209
+ : 'package_public_name_not_exposed',
210
+ {
211
+ candidateSetComplete: surface.omitted === 0,
212
+ publicSurface: surfaceCounts,
213
+ },
214
+ );
84
215
  }
85
216
 
86
- function exportsByRepo(db: Db, workspaceId: number): Map<number, RepoExports> {
87
- const result = new Map<number, RepoExports>();
88
- const rows = db.prepare(`SELECT s.id,s.repo_id repoId,s.name,s.exported_name exportedName,
89
- s.qualified_name qualifiedName,s.source_file sourceFile
90
- FROM symbols s JOIN repositories r ON r.id=s.repo_id
91
- WHERE r.workspace_id=? AND s.exported=1 ORDER BY s.repo_id,s.id`).all(workspaceId);
92
- for (const row of rows) addExportRow(result, row);
93
- return result;
217
+ function symbolEvidenceMatches(
218
+ evidenceJson: unknown,
219
+ target: PublicSurfaceTarget,
220
+ scope: PackagePublicScope,
221
+ ): boolean {
222
+ const evidence = parseRecord(evidenceJson);
223
+ if (!evidence) return false;
224
+ const surface = record(evidence.packagePublicSurface);
225
+ if (!surface) return false;
226
+ const body = record(surface.bodyEligibility);
227
+ const exposures = surface.exposures;
228
+ if (!body || !Array.isArray(exposures)) return false;
229
+ const exactProof = [
230
+ body.eligible === true,
231
+ body.reason === target.bodyEligibility.reason,
232
+ surface.recordCap === 256,
233
+ surface.exposureTotal === exposures.length,
234
+ surface.shownExposureCount === exposures.length,
235
+ surface.omittedExposureCount === 0,
236
+ ];
237
+ if (exactProof.includes(false)) return false;
238
+ return exposures.some((item) => {
239
+ const exposure = record(item);
240
+ return exposure?.entry === scope.entry
241
+ && exposure.modulePath === scope.modulePath
242
+ && exposure.publicName === scope.publicName;
243
+ });
94
244
  }
95
245
 
96
- function addExportRow(result: Map<number, RepoExports>, row: Record<string, unknown>): void {
97
- if (typeof row.id !== 'number' || typeof row.repoId !== 'number') return;
98
- const exports = result.get(row.repoId) ?? emptyRepoExports();
99
- result.set(row.repoId, exports);
100
- const publicName = nullableString(row.exportedName) ?? nullableString(row.name);
101
- const qualifiedName = nullableString(row.qualifiedName);
102
- const sourceFile = nullableString(row.sourceFile);
103
- if (publicName) push(exports.publicName, publicName, row.id);
104
- if (qualifiedName) push(exports.qualified, qualifiedName, row.id);
105
- if (sourceFile) exports.fileById.set(row.id, stripExt(sourceFile));
246
+ function targetSymbolIds(
247
+ db: Db,
248
+ repositoryId: number,
249
+ scope: PackagePublicScope,
250
+ ): number[] {
251
+ return scope.targets.flatMap((target) => {
252
+ if (!target.bodyEligibility.eligible) return [];
253
+ const rows = db.prepare(`SELECT id,evidence_json evidenceJson FROM symbols
254
+ WHERE repo_id=? AND source_file=? AND kind=? AND qualified_name=?
255
+ AND start_offset=? AND end_offset=?
256
+ ORDER BY id`).all(
257
+ repositoryId,
258
+ target.sourceFile,
259
+ target.kind,
260
+ target.qualifiedName,
261
+ target.startOffset,
262
+ target.endOffset,
263
+ );
264
+ return rows.flatMap((row) =>
265
+ typeof row.id === 'number'
266
+ && symbolEvidenceMatches(row.evidenceJson, target, scope)
267
+ ? [row.id] : []);
268
+ });
106
269
  }
107
270
 
108
- function packageCallRows(db: Db, workspaceId: number): PackageCallRow[] {
109
- const rows = db.prepare(`SELECT sc.id,sc.repo_id callerRepoId,
110
- sc.callee_expression calleeExpression,sc.import_source importSource,
111
- sc.evidence_json evidenceJson
112
- FROM symbol_calls sc JOIN repositories r ON r.id=sc.repo_id
113
- WHERE r.workspace_id=? AND sc.import_source IS NOT NULL
114
- AND sc.import_source NOT LIKE './%' AND sc.import_source NOT LIKE '../%'
115
- AND json_extract(sc.evidence_json,'$.relation')='package_import'
116
- AND json_extract(sc.evidence_json,'$.candidateStrategy') IN
117
- ('package_import_unresolved','package_import_workspace_resolved','package_import_ambiguous')
118
- ORDER BY sc.id`).all(workspaceId);
119
- return rows.flatMap((row) => packageCallRow(row));
120
- }
121
-
122
- function packageCallRow(row: Record<string, unknown>): PackageCallRow[] {
123
- const calleeExpression = nullableString(row.calleeExpression);
124
- const importSource = nullableString(row.importSource);
125
- if (typeof row.id !== 'number' || typeof row.callerRepoId !== 'number'
126
- || !calleeExpression || !importSource) return [];
127
- return [{
128
- id: row.id,
129
- callerRepoId: row.callerRepoId,
130
- calleeExpression,
131
- importSource,
132
- evidence: evidenceObject(row.evidenceJson),
133
- }];
134
- }
135
-
136
- function candidatesForCall(call: PackageCallRow, exports: RepoExports | undefined): number[] {
137
- if (!exports) return [];
138
- const dotCount = [...call.calleeExpression].filter((character) => character === '.').length;
139
- if (dotCount === 0) {
140
- const targetName = nullableString(call.evidence.targetName);
141
- return targetName ? exports.publicName.get(targetName) ?? [] : [];
142
- }
143
- return dotCount === 1 ? exports.qualified.get(call.calleeExpression) ?? [] : [];
271
+ function scopeResolution(
272
+ db: Db,
273
+ repository: PackageRepository,
274
+ scope: PackagePublicScope,
275
+ ): PackageCallResolution {
276
+ const ids = [...new Set(targetSymbolIds(db, repository.id, scope))];
277
+ if (ids.length !== scope.eligibleCandidateCount) return unresolved(
278
+ 'public_surface_evidence_incomplete',
279
+ {
280
+ candidateCount: scope.candidateCount,
281
+ eligibleCandidateCount: ids.length,
282
+ candidateSetComplete: false,
283
+ resolvedModulePath: scope.modulePath,
284
+ targetRepoId: repository.id,
285
+ repositoryCandidateIds: [repository.id],
286
+ },
287
+ );
288
+ const base = {
289
+ candidateCount: scope.candidateCount,
290
+ eligibleCandidateCount: scope.eligibleCandidateCount,
291
+ selectedCandidateCount: 0 as const,
292
+ candidateSetComplete: scope.candidateSetComplete,
293
+ resolvedModulePath: scope.modulePath,
294
+ targetRepoId: repository.id,
295
+ repositoryCandidateIds: [repository.id],
296
+ };
297
+ if (ids.length === 1 && ids[0] !== undefined) return {
298
+ ...base,
299
+ id: ids[0],
300
+ status: 'resolved',
301
+ reason: null,
302
+ strategy: 'package_public_surface_exact',
303
+ selectedCandidateCount: 1,
304
+ };
305
+ if (ids.length > 1) return {
306
+ ...base,
307
+ id: null,
308
+ status: 'ambiguous',
309
+ reason: 'package_public_target_ambiguous',
310
+ strategy: 'package_public_surface_ambiguous',
311
+ };
312
+ return unresolved(
313
+ scope.candidateCount > 0 && scope.eligibleCandidateCount === 0
314
+ ? 'public_symbol_has_no_executable_body'
315
+ : 'package_public_name_not_exposed',
316
+ base,
317
+ );
144
318
  }
145
319
 
146
320
  function resolvePackageCall(
321
+ db: Db,
147
322
  call: PackageCallRow,
148
- repos: Map<string, number | null>,
149
- exports: Map<number, RepoExports>,
323
+ repositories: Map<string, PackageRepository[]>,
150
324
  ): PackageCallResolution {
151
- const targetRepoId = packageRepoId(repos, call.importSource);
152
- if (targetRepoId === null || targetRepoId === call.callerRepoId) return unresolvedResolution(unresolvedRepositoryReason);
153
- const repoExports = exports.get(targetRepoId);
154
- const candidates = candidatesForCall(call, repoExports);
155
- const [id] = candidates;
156
- if (candidates.length === 1 && id !== undefined) {
157
- return {
158
- id, status: 'resolved', reason: null, strategy: 'package_import_workspace_resolved',
159
- candidateCount: 1, resolvedModulePath: repoExports?.fileById.get(id),
160
- };
161
- }
162
- if (candidates.length > 1) {
163
- return { id: null, status: 'ambiguous', reason: ambiguousSymbolReason, strategy: 'package_import_ambiguous', candidateCount: candidates.length };
164
- }
165
- return unresolvedResolution(unresolvedSymbolReason);
325
+ const repository = repositoryScopeResolution(call, repositories);
326
+ if ('status' in repository) return repository;
327
+ const scope = publicScope(repository.surface, call.binding);
328
+ if ('status' in scope) return {
329
+ ...scope,
330
+ targetRepoId: repository.id,
331
+ repositoryCandidateIds: [repository.id],
332
+ };
333
+ return scopeResolution(db, repository, scope);
334
+ }
335
+
336
+ export function expectedPackageImportResolutions(
337
+ db: Db,
338
+ workspaceId: number,
339
+ inputs: readonly PackageResolutionInput[],
340
+ ): PackageResolutionResult[] {
341
+ const repositories = repositoriesByPackage(db, workspaceId);
342
+ return inputs.map((input) => ({
343
+ callId: input.callId,
344
+ ...resolvePackageCall(
345
+ db,
346
+ {
347
+ id: input.callId,
348
+ callerRepoId: 0,
349
+ binding: input.binding,
350
+ evidence: {},
351
+ },
352
+ repositories,
353
+ ),
354
+ }));
166
355
  }
167
356
 
168
- function unresolvedResolution(reason: string): PackageCallResolution {
169
- return { id: null, status: 'unresolved', reason, strategy: 'package_import_unresolved', candidateCount: 0 };
357
+ const resolverEvidenceKeys = new Set([
358
+ 'candidateStrategy', 'candidateCount', 'eligibleCandidateCount',
359
+ 'selectedCandidateCount', 'candidateSetComplete', 'resolvedModulePath',
360
+ 'resolvedTargetRepositoryId', 'unresolvedReason',
361
+ 'targetRepositoryCandidateCount', 'targetRepositoryCandidates',
362
+ 'shownTargetRepositoryCandidateCount',
363
+ 'omittedTargetRepositoryCandidateCount', 'publicSurface',
364
+ ]);
365
+
366
+ function parserEvidence(
367
+ evidence: Record<string, unknown>,
368
+ ): Record<string, unknown> {
369
+ return Object.fromEntries(Object.entries(evidence).filter(
370
+ ([key]) => !resolverEvidenceKeys.has(key),
371
+ ));
170
372
  }
171
373
 
172
- function resolutionEvidence(call: PackageCallRow, resolution: PackageCallResolution): string {
173
- const evidence: Record<string, unknown> = {
174
- ...call.evidence,
374
+ function resolutionEvidence(
375
+ call: PackageCallRow,
376
+ resolution: PackageCallResolution,
377
+ ): string {
378
+ const ids = resolution.repositoryCandidateIds
379
+ .slice(0, REPOSITORY_REFERENCE_CAP);
380
+ return JSON.stringify({
381
+ ...parserEvidence(call.evidence),
175
382
  candidateStrategy: resolution.strategy,
176
383
  candidateCount: resolution.candidateCount,
177
- };
178
- if (resolution.resolvedModulePath) evidence.resolvedModulePath = resolution.resolvedModulePath;
179
- else delete evidence.resolvedModulePath;
180
- return JSON.stringify(evidence);
384
+ eligibleCandidateCount: resolution.eligibleCandidateCount,
385
+ selectedCandidateCount: resolution.selectedCandidateCount,
386
+ candidateSetComplete: resolution.candidateSetComplete,
387
+ unresolvedReason: resolution.reason,
388
+ ...(resolution.resolvedModulePath
389
+ ? { resolvedModulePath: resolution.resolvedModulePath } : {}),
390
+ ...(resolution.targetRepoId
391
+ ? { resolvedTargetRepositoryId: resolution.targetRepoId } : {}),
392
+ targetRepositoryCandidateCount:
393
+ resolution.repositoryCandidateIds.length,
394
+ targetRepositoryCandidates: ids,
395
+ shownTargetRepositoryCandidateCount: ids.length,
396
+ omittedTargetRepositoryCandidateCount:
397
+ resolution.repositoryCandidateIds.length - ids.length,
398
+ ...(resolution.publicSurface
399
+ ? { publicSurface: resolution.publicSurface } : {}),
400
+ });
181
401
  }
182
402
 
183
- export function linkPackageImportSymbolCalls(db: Db, workspaceId: number): PackageSymbolLinkSummary {
184
- const repos = repoByPackageName(db, workspaceId);
185
- const exports = exportsByRepo(db, workspaceId);
186
- const update = db.prepare(`UPDATE symbol_calls SET callee_symbol_id=?,status=?,
187
- unresolved_reason=?,evidence_json=? WHERE id=?`);
188
- const summary: PackageSymbolLinkSummary = { resolved: 0, ambiguous: 0, unresolved: 0 };
403
+ export function linkPackageImportSymbolCalls(
404
+ db: Db,
405
+ workspaceId: number,
406
+ ): PackageSymbolLinkSummary {
407
+ const repositories = repositoriesByPackage(db, workspaceId);
408
+ const update = db.prepare(`UPDATE symbol_calls SET callee_symbol_id=?,
409
+ status=?,unresolved_reason=?,evidence_json=? WHERE id=?`);
410
+ const summary: PackageSymbolLinkSummary = {
411
+ resolved: 0,
412
+ ambiguous: 0,
413
+ unresolved: 0,
414
+ };
189
415
  for (const call of packageCallRows(db, workspaceId)) {
190
- const resolution = resolvePackageCall(call, repos, exports);
191
- update.run(resolution.id, resolution.status, resolution.reason, resolutionEvidence(call, resolution), call.id);
416
+ const resolution = resolvePackageCall(db, call, repositories);
417
+ update.run(
418
+ resolution.id,
419
+ resolution.status,
420
+ resolution.reason,
421
+ resolutionEvidence(call, resolution),
422
+ call.id,
423
+ );
192
424
  summary[resolution.status] += 1;
193
425
  }
194
426
  return summary;
@@ -61,6 +61,8 @@ function subscriptionRows(db: Db, workspaceId: number): SubscriptionRow[] {
61
61
  c.confidence
62
62
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
63
63
  WHERE r.workspace_id=? AND c.call_type='async_subscribe'
64
+ AND json_extract(c.evidence_json,'$.handlerReferenceStatus')
65
+ ='role_required'
64
66
  ORDER BY r.name COLLATE BINARY,r.id,c.source_file COLLATE BINARY,
65
67
  c.call_site_start_offset,c.call_site_end_offset,c.id`).all(
66
68
  workspaceId,