@saptools/service-flow 0.1.57 → 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/CHANGELOG.md +6 -0
- package/dist/{chunk-DCOFNDKS.js → chunk-CRSSXWQ2.js} +149 -1
- package/dist/chunk-CRSSXWQ2.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/linker/003-package-import-symbol-resolver.ts +195 -0
- package/src/linker/cross-repo-linker.ts +2 -0
- package/dist/chunk-DCOFNDKS.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -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);
|