@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,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;
@@ -1,4 +1,8 @@
1
1
  import type { Db } from '../db/connection.js';
2
+ import {
3
+ linkEventTemplate,
4
+ type LinkedEventTemplate,
5
+ } from './006-event-template-link.js';
2
6
 
3
7
  export interface SubscriptionHandlerLinkSummary {
4
8
  edgeCount: number;
@@ -20,6 +24,7 @@ interface SubscriptionRow {
20
24
  startOffset?: number | null;
21
25
  endOffset?: number | null;
22
26
  confidence: number;
27
+ unresolvedReason?: string | null;
23
28
  }
24
29
 
25
30
  interface HandlerCallRow {
@@ -58,9 +63,11 @@ function subscriptionRows(db: Db, workspaceId: number): SubscriptionRow[] {
58
63
  c.source_symbol_id sourceSymbolId,c.event_name_expr eventName,
59
64
  c.source_file sourceFile,c.source_line sourceLine,
60
65
  c.call_site_start_offset startOffset,c.call_site_end_offset endOffset,
61
- c.confidence
66
+ c.confidence,c.unresolved_reason unresolvedReason
62
67
  FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
63
68
  WHERE r.workspace_id=? AND c.call_type='async_subscribe'
69
+ AND json_extract(c.evidence_json,'$.handlerReferenceStatus')
70
+ ='role_required'
64
71
  ORDER BY r.name COLLATE BINARY,r.id,c.source_file COLLATE BINARY,
65
72
  c.call_site_start_offset,c.call_site_end_offset,c.id`).all(
66
73
  workspaceId,
@@ -200,11 +207,16 @@ function missingAssociation(
200
207
  function evidenceFor(
201
208
  subscription: SubscriptionRow,
202
209
  association: HandlerAssociation,
210
+ event: LinkedEventTemplate,
203
211
  ): Record<string, unknown> {
204
212
  const call: Partial<HandlerCallRow> = association.call ?? {};
205
213
  const symbolCallReason = boundedSymbolCallReason(call.unresolvedReason);
206
214
  return {
207
215
  eventName: subscription.eventName,
216
+ ...(event.substitution.placeholders.length > 0 ? {
217
+ effectiveEventName: event.targetId,
218
+ eventTemplateResolution: event.substitution,
219
+ } : {}),
208
220
  associationBasis: 'exact_subscription_call_span',
209
221
  dispatchScope: 'workspace_event_name_only',
210
222
  subscribeCallId: subscription.id,
@@ -251,22 +263,27 @@ function insertAssociationEdge(
251
263
  generation: number,
252
264
  subscription: SubscriptionRow,
253
265
  association: HandlerAssociation,
266
+ event: LinkedEventTemplate,
254
267
  ): void {
268
+ const status = event.isDynamic ? 'unresolved' : association.status;
269
+ const reason = event.isDynamic
270
+ ? 'event_template_variables_missing'
271
+ : association.reasonCode ?? association.call?.unresolvedReason ?? null;
255
272
  db.prepare(`INSERT INTO graph_edges(
256
273
  workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,
257
274
  confidence,evidence_json,is_dynamic,unresolved_reason,generation
258
275
  ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(
259
276
  workspaceId,
260
277
  'EVENT_SUBSCRIPTION_HANDLED_BY',
261
- association.status,
262
- 'event',
263
- subscription.eventName,
278
+ status,
279
+ event.targetKind,
280
+ event.targetId,
264
281
  association.toKind,
265
282
  association.toId,
266
283
  association.call?.confidence ?? subscription.confidence,
267
- JSON.stringify(evidenceFor(subscription, association)),
268
- 0,
269
- association.reasonCode ?? association.call?.unresolvedReason ?? null,
284
+ JSON.stringify(evidenceFor(subscription, association, event)),
285
+ event.isDynamic ? 1 : 0,
286
+ reason,
270
287
  generation,
271
288
  );
272
289
  }
@@ -275,6 +292,7 @@ export function linkEventSubscriptionHandlers(
275
292
  db: Db,
276
293
  workspaceId: number,
277
294
  generation: number,
295
+ variables: Record<string, string> = {},
278
296
  ): SubscriptionHandlerLinkSummary {
279
297
  const summary: SubscriptionHandlerLinkSummary = {
280
298
  edgeCount: 0,
@@ -284,16 +302,21 @@ export function linkEventSubscriptionHandlers(
284
302
  missingAssociationCount: 0,
285
303
  };
286
304
  for (const subscription of subscriptionRows(db, workspaceId)) {
305
+ const event = linkEventTemplate(
306
+ subscription.eventName, variables,
307
+ subscription.unresolvedReason ?? undefined,
308
+ );
287
309
  const association = associationFor(
288
310
  subscription, roleSiteRows(db, subscription),
289
311
  );
290
312
  insertAssociationEdge(
291
- db, workspaceId, generation, subscription, association,
313
+ db, workspaceId, generation, subscription, association, event,
292
314
  );
315
+ const status = event.isDynamic ? 'unresolved' : association.status;
293
316
  summary.edgeCount += 1;
294
- summary.resolvedCount += association.status === 'resolved' ? 1 : 0;
295
- summary.ambiguousCount += association.status === 'ambiguous' ? 1 : 0;
296
- summary.unresolvedCount += association.status === 'unresolved' ? 1 : 0;
317
+ summary.resolvedCount += status === 'resolved' ? 1 : 0;
318
+ summary.ambiguousCount += status === 'ambiguous' ? 1 : 0;
319
+ summary.unresolvedCount += status === 'unresolved' ? 1 : 0;
297
320
  summary.missingAssociationCount += association.missing ? 1 : 0;
298
321
  }
299
322
  return summary;