@saptools/service-flow 0.1.56 → 0.1.58

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.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-Y7H7ZU5B.js";
20
+ } from "./chunk-CRSSXWQ2.js";
21
21
 
22
22
  // src/parsers/generated-constants-parser.ts
23
23
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,3 +1,4 @@
1
+ import { posix } from 'node:path';
1
2
  import type { Db } from './connection.js';
2
3
  import { projectBounded } from '../utils/000-bounded-projection.js';
3
4
  import type {
@@ -390,34 +391,88 @@ export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]
390
391
  for (const r of rows) {
391
392
  const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName) as { id?: number } | undefined;
392
393
  const target = resolveSymbolCallTarget(db, repoId, r);
393
- insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
394
+ insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount, resolvedModulePath: target.resolvedModulePath }), target.reason);
394
395
  }
395
396
  }
397
+ interface SymbolTargetRow { id: number; kind?: string; sourceFile?: string | null; evidenceJson?: string | null }
398
+ interface SymbolCallResolution { id: number | null; status: 'resolved' | 'ambiguous' | 'unresolved'; reason: string | null; strategy: string; candidateCount: number; resolvedModulePath?: string }
399
+ const stripExt = (value: string): string => value.replace(/\.(ts|tsx|js|jsx|cds)$/, '');
400
+ function symbolTargetRows(rows: Array<Record<string, unknown>>): SymbolTargetRow[] {
401
+ return rows.flatMap((row) => typeof row.id === 'number' ? [{ id: row.id, kind: typeof row.kind === 'string' ? row.kind : undefined, sourceFile: nullableString(row.sourceFile), evidenceJson: nullableString(row.evidenceJson) }] : []);
402
+ }
403
+ function relativeModuleTargets(callerSourceFile: string, importSource: string): Set<string> {
404
+ const base = posix.dirname(callerSourceFile);
405
+ const joined = stripExt(posix.normalize(posix.join(base, importSource)));
406
+ return new Set([joined, `${joined}/index`]);
407
+ }
408
+ function moduleRows(rows: SymbolTargetRow[], r: SymbolCallFact): SymbolTargetRow[] {
409
+ if (!r.importSource) return [];
410
+ const targets = relativeModuleTargets(r.sourceFile, r.importSource);
411
+ return rows.filter((row) => typeof row.sourceFile === 'string' && targets.has(stripExt(row.sourceFile)));
412
+ }
413
+ function resolvedSymbol(row: SymbolTargetRow, strategy: string, candidateCount: number, moduleScoped = false): SymbolCallResolution {
414
+ return { id: row.id, status: 'resolved', reason: null, strategy, candidateCount, resolvedModulePath: moduleScoped && row.sourceFile ? stripExt(row.sourceFile) : undefined };
415
+ }
416
+ function exportedSymbolRows(db: Db, repoId: number, r: SymbolCallFact): SymbolTargetRow[] {
417
+ 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));
418
+ }
396
419
  function isRelativeImportedSymbolCall(r: SymbolCallFact): boolean {
397
420
  return Boolean(r.importSource?.startsWith('.'));
398
421
  }
399
- function resolveSymbolCallTarget(db: Db, repoId: number, r: SymbolCallFact): { id: number | null; status: string; reason: string | null; strategy: string; candidateCount: number } {
400
- const evidence = r.evidence as { relation?: unknown };
401
- const localRows = 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) as Array<{ id: number }>;
402
- if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: 'resolved', reason: null, strategy: 'same_file_exact', candidateCount: 1 };
403
- if (localRows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple same-file symbol targets matched exactly', strategy: 'same_file_exact', candidateCount: localRows.length };
404
- if (evidence.relation === 'class_instance_method' && isRelativeImportedSymbolCall(r)) {
405
- const classRows = db.prepare('SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id').all(repoId, r.sourceFile, r.calleeLocalName) as Array<{ id: number }>;
406
- if (classRows.length === 1) return { id: classRows[0]?.id ?? null, status: 'resolved', reason: null, strategy: 'relative_import_class_instance_method', candidateCount: 1 };
407
- if (classRows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple relative class instance method targets matched exactly', strategy: 'relative_import_class_instance_method', candidateCount: classRows.length };
408
- }
409
- const rows = db.prepare('SELECT id,kind,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) as Array<{ id: number; kind?: string; evidenceJson?: string | null }>;
410
- if (evidence.relation === 'relative_import_proxy_member' && rows.length > 1) {
411
- const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? '').includes('exported_object_shorthand') || String(row.evidenceJson ?? '').includes('exported_object_literal'));
412
- if (objectMapRows.length > 0) {
413
- const concrete = rows.find((row) => row.kind !== 'object_alias') ?? objectMapRows[0];
414
- return { id: concrete?.id ?? null, status: 'resolved', reason: null, strategy: 'proxy_member_exported_object_map', candidateCount: rows.length };
415
- }
416
- return { id: null, status: 'ambiguous', reason: 'Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous', strategy: 'proxy_member_no_global_name_fallback', candidateCount: rows.length };
422
+ function sameFileResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
423
+ const bareImport = relation === 'relative_import' && isRelativeImportedSymbolCall(r) && !String(r.calleeLocalName).includes('.');
424
+ if (bareImport || relation === 'relative_import_namespace_member' || relation === 'package_import') return undefined;
425
+ 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));
426
+ if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'same_file_exact', 1);
427
+ return rows.length > 1 ? { id: null, status: 'ambiguous', reason: 'Multiple same-file symbol targets matched exactly', strategy: 'same_file_exact', candidateCount: rows.length } : undefined;
428
+ }
429
+ function classInstanceResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
430
+ if (relation !== 'class_instance_method' || !isRelativeImportedSymbolCall(r)) return undefined;
431
+ 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));
432
+ if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'relative_import_class_instance_method', 1);
433
+ return rows.length > 1 ? { id: null, status: 'ambiguous', reason: 'Multiple relative class instance method targets matched exactly', strategy: 'relative_import_class_instance_method', candidateCount: rows.length } : undefined;
434
+ }
435
+ function namespaceResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
436
+ if (relation !== 'relative_import_namespace_member' || !isRelativeImportedSymbolCall(r)) return undefined;
437
+ const rows = moduleRows(exportedSymbolRows(db, repoId, r), r);
438
+ if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], 'relative_import_namespace_member', 1, true);
439
+ if (rows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple namespace member targets matched the imported module', strategy: 'relative_import_namespace_member', candidateCount: rows.length };
440
+ return { id: null, status: 'unresolved', reason: 'No namespace member target matched the imported module', strategy: 'relative_import_namespace_member', candidateCount: 0 };
441
+ }
442
+ function proxyResolution(rows: SymbolTargetRow[], r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
443
+ if (relation !== 'relative_import_proxy_member' || rows.length <= 1) return undefined;
444
+ const mapped = rows.filter((row) => String(row.evidenceJson ?? '').includes('exported_object_shorthand') || String(row.evidenceJson ?? '').includes('exported_object_literal'));
445
+ if (mapped.length > 0) {
446
+ const concrete = rows.find((row) => row.kind !== 'object_alias') ?? mapped[0];
447
+ return { id: concrete?.id ?? null, status: 'resolved', reason: null, strategy: 'proxy_member_exported_object_map', candidateCount: rows.length };
417
448
  }
418
- if (rows.length === 1) return { id: rows[0]?.id ?? null, status: 'resolved', reason: null, strategy: evidence.relation === 'relative_import_proxy_member' ? 'proxy_member_unique_exported_candidate' : 'relative_import_exported_exact', candidateCount: 1 };
419
- if (rows.length > 1) return { id: null, status: 'ambiguous', reason: 'Multiple exported symbol targets matched exactly', strategy: 'exported_exact', candidateCount: rows.length };
420
- return { id: null, status: 'unresolved', reason: 'No local symbol target matched exactly', strategy: evidence.relation === 'relative_import_proxy_member' ? 'proxy_member_no_global_name_fallback' : 'exact_symbol_match', candidateCount: 0 };
449
+ const scoped = moduleRows(rows, r);
450
+ if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], 'relative_import_path_disambiguated', rows.length, true);
451
+ return { id: null, status: 'ambiguous', reason: 'Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous', strategy: 'proxy_member_no_global_name_fallback', candidateCount: rows.length };
452
+ }
453
+ function exportedResolution(rows: SymbolTargetRow[], r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
454
+ if (rows.length === 1 && rows[0]) return resolvedSymbol(rows[0], relation === 'relative_import_proxy_member' ? 'proxy_member_unique_exported_candidate' : 'relative_import_exported_exact', 1, moduleRows(rows, r).length === 1);
455
+ if (rows.length <= 1) return undefined;
456
+ const scoped = isRelativeImportedSymbolCall(r) ? moduleRows(rows, r) : [];
457
+ if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], 'relative_import_path_disambiguated', rows.length, true);
458
+ return { id: null, status: 'ambiguous', reason: 'Multiple exported symbol targets matched exactly', strategy: 'exported_exact', candidateCount: rows.length };
459
+ }
460
+ function accessorResolution(db: Db, repoId: number, r: SymbolCallFact, relation: unknown): SymbolCallResolution | undefined {
461
+ if (relation !== 'relative_import' || !isRelativeImportedSymbolCall(r) || !/^[^.]+\.[^.]+$/.test(String(r.calleeLocalName))) return undefined;
462
+ 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));
463
+ const scoped = moduleRows(methodRows, r);
464
+ if (scoped.length === 1 && scoped[0]) return resolvedSymbol(scoped[0], 'relative_import_static_accessor_instance_method', 1, true);
465
+ return scoped.length > 1 ? { id: null, status: 'ambiguous', reason: 'Multiple static-accessor instance method targets matched the imported module', strategy: 'relative_import_static_accessor_instance_method', candidateCount: scoped.length } : undefined;
466
+ }
467
+ function resolveSymbolCallTarget(db: Db, repoId: number, r: SymbolCallFact): SymbolCallResolution {
468
+ const relation = r.evidence.relation;
469
+ const early = sameFileResolution(db, repoId, r, relation) ?? classInstanceResolution(db, repoId, r, relation) ?? namespaceResolution(db, repoId, r, relation);
470
+ if (early) return early;
471
+ const rows = relation === 'package_import' ? [] : exportedSymbolRows(db, repoId, r);
472
+ const matched = proxyResolution(rows, r, relation) ?? exportedResolution(rows, r, relation) ?? accessorResolution(db, repoId, r, relation);
473
+ if (matched) return matched;
474
+ if (relation === 'package_import') return { id: null, status: 'unresolved', reason: 'Package import target resolution requires a post-publication workspace pass', strategy: 'package_import_unresolved', candidateCount: 0 };
475
+ return { id: null, status: 'unresolved', reason: 'No local symbol target matched exactly', strategy: relation === 'relative_import_proxy_member' ? 'proxy_member_no_global_name_fallback' : 'exact_symbol_match', candidateCount: 0 };
421
476
  }
422
477
  export function insertCalls(
423
478
  db: Db,
@@ -0,0 +1,195 @@
1
+ import type { Db } from '../db/connection.js';
2
+
3
+ export interface PackageSymbolLinkSummary {
4
+ resolved: number;
5
+ ambiguous: number;
6
+ unresolved: number;
7
+ }
8
+
9
+ interface RepoExports {
10
+ publicName: Map<string, number[]>;
11
+ qualified: Map<string, number[]>;
12
+ fileById: Map<number, string>;
13
+ }
14
+
15
+ interface PackageCallRow {
16
+ id: number;
17
+ callerRepoId: number;
18
+ calleeExpression: string;
19
+ importSource: string;
20
+ evidence: Record<string, unknown>;
21
+ }
22
+
23
+ interface PackageCallResolution {
24
+ id: number | null;
25
+ status: 'resolved' | 'ambiguous' | 'unresolved';
26
+ reason: string | null;
27
+ strategy: 'package_import_workspace_resolved' | 'package_import_ambiguous' | 'package_import_unresolved';
28
+ candidateCount: number;
29
+ resolvedModulePath?: string;
30
+ }
31
+
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)$/, '');
36
+
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]);
41
+ }
42
+
43
+ function nullableString(value: unknown): string | undefined {
44
+ return typeof value === 'string' ? value : undefined;
45
+ }
46
+
47
+ function evidenceObject(value: unknown): Record<string, unknown> {
48
+ if (typeof value !== 'string' || value.length === 0) return {};
49
+ 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
+ : {};
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
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);
63
+ 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);
67
+ }
68
+ return result;
69
+ }
70
+
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;
75
+ }
76
+
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;
80
+ }
81
+
82
+ function emptyRepoExports(): RepoExports {
83
+ return { publicName: new Map(), qualified: new Map(), fileById: new Map() };
84
+ }
85
+
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;
94
+ }
95
+
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));
106
+ }
107
+
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) ?? [] : [];
144
+ }
145
+
146
+ function resolvePackageCall(
147
+ call: PackageCallRow,
148
+ repos: Map<string, number | null>,
149
+ exports: Map<number, RepoExports>,
150
+ ): 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);
166
+ }
167
+
168
+ function unresolvedResolution(reason: string): PackageCallResolution {
169
+ return { id: null, status: 'unresolved', reason, strategy: 'package_import_unresolved', candidateCount: 0 };
170
+ }
171
+
172
+ function resolutionEvidence(call: PackageCallRow, resolution: PackageCallResolution): string {
173
+ const evidence: Record<string, unknown> = {
174
+ ...call.evidence,
175
+ candidateStrategy: resolution.strategy,
176
+ candidateCount: resolution.candidateCount,
177
+ };
178
+ if (resolution.resolvedModulePath) evidence.resolvedModulePath = resolution.resolvedModulePath;
179
+ else delete evidence.resolvedModulePath;
180
+ return JSON.stringify(evidence);
181
+ }
182
+
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 };
189
+ 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);
192
+ summary[resolution.status] += 1;
193
+ }
194
+ return summary;
195
+ }
@@ -12,6 +12,7 @@ import {
12
12
  objectJson,
13
13
  objectValue,
14
14
  } from './002-call-evidence.js';
15
+ import { linkPackageImportSymbolCalls } from './003-package-import-symbol-resolver.js';
15
16
  export interface LinkWorkspaceResult {
16
17
  edgeCount: number;
17
18
  unresolvedCount: number;
@@ -32,6 +33,7 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
32
33
  const generation = nextGraphGeneration(db, workspaceId);
33
34
  db.prepare('DELETE FROM graph_edges WHERE workspace_id=?').run(workspaceId);
34
35
  const deps = linkHelperPackages(db, workspaceId, generation);
36
+ linkPackageImportSymbolCalls(db, workspaceId);
35
37
  const impl = linkCanonicalImplementations(db, workspaceId, generation);
36
38
  const callSummary = linkCalls(db, workspaceId, vars, generation);
37
39
  db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
@@ -184,6 +184,7 @@ export async function parseExecutableSymbols(
184
184
  const symbols: ExecutableSymbolFact[] = [];
185
185
  const calls: SymbolCallFact[] = [];
186
186
  const imports = new Map<string, string>();
187
+ const namespaceImports = new Set<string>();
187
188
  const exportNames = exportDeclarations(source);
188
189
  const objectExports = new Set<string>();
189
190
  const exportedClasses = new Set<string>();
@@ -214,7 +215,10 @@ export async function parseExecutableSymbols(
214
215
  if (clause?.name) imports.set(clause.name.text, sourceText);
215
216
  const named = clause?.namedBindings;
216
217
  if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
217
- if (named && ts.isNamespaceImport(named)) imports.set(named.name.text, sourceText);
218
+ if (named && ts.isNamespaceImport(named)) {
219
+ imports.set(named.name.text, sourceText);
220
+ namespaceImports.add(named.name.text);
221
+ }
218
222
  }
219
223
  if (ts.isVariableStatement(node)) {
220
224
  for (const declaration of node.declarationList.declarations) {
@@ -330,7 +334,9 @@ export async function parseExecutableSymbols(
330
334
  const instance = (callee.local ? classInstances.get(callee.local) : undefined) ?? (callee.receiver ? classInstances.get(callee.receiver) : undefined);
331
335
  const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : undefined) ?? (callee.member && callee.local ? imports.get(callee.local) : undefined);
332
336
  const directThisMethod = callee.receiver === 'this';
333
- const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
337
+ const namespaceMember = Boolean(callee.member && callee.local && namespaceImports.has(callee.local));
338
+ const packageImport = Boolean(importSource && !isRelativeImport(importSource));
339
+ const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : directThisMethod ? callee.member : namespaceMember ? callee.member : packageImport ? (callee.member ?? callee.local) : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
334
340
  const className = caller.qualifiedName.includes('.') ? caller.qualifiedName.split('.')[0] : undefined;
335
341
  const thisTarget = directThisMethod && className && callee.member ? `${className}.${callee.member}` : undefined;
336
342
  const loggerLike = callee.receiver?.endsWith('.logger') || callee.local === 'logger' || (callee.expression.startsWith('this.logger.') && callee.member ? loggerMembers.has(callee.member) : false);
@@ -339,11 +345,11 @@ export async function parseExecutableSymbols(
339
345
  const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
340
346
  const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
341
347
  const provenClassInstance = Boolean(instance && callee.member && targetName);
342
- const importedFromPackage = Boolean(importSource && !isRelativeImport(importSource));
343
- const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
348
+ const provenPackageImport = Boolean(packageImport && targetName);
349
+ const ignored = loggerLike || terminalMember || ignoredFrameworkCall(callee);
344
350
  const resolvedTarget = provenThisMethod ? thisTarget : targetName;
345
- const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance);
346
- if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? (callee.local ?? callee.receiver) : undefined, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? 'class_instance_method' : proxy ? 'relative_import_proxy_member' : importSource ? 'relative_import' : provenThisMethod ? 'indexed_this_method' : 'indexed_local_symbol', caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? (instance.propertyName ?? callee.local) : undefined, className: instance?.className, methodName: instance ? callee.member : undefined, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? (instance.importSource ? 'relative_import_class_instance_method' : 'same_file_class_instance_method') : proxy ? 'proxy_member_exact_export_or_unique_member' : undefined } });
351
+ const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance || provenPackageImport);
352
+ if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? (callee.local ?? callee.receiver) : undefined, importSource, sourceFile, sourceLine: line, evidence: { relation: instance ? 'class_instance_method' : proxy ? 'relative_import_proxy_member' : packageImport ? 'package_import' : namespaceMember ? 'relative_import_namespace_member' : importSource ? 'relative_import' : provenThisMethod ? 'indexed_this_method' : 'indexed_local_symbol', caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? (instance.propertyName ?? callee.local) : undefined, className: instance?.className, methodName: instance ? callee.member : undefined, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? (instance.importSource ? 'relative_import_class_instance_method' : 'same_file_class_instance_method') : proxy ? 'proxy_member_exact_export_or_unique_member' : undefined } });
347
353
  }
348
354
  }
349
355
  ts.forEachChild(node, visitCalls);