@mortice/mapwright 0.8.0
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 +161 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/catalogue/canonicalisation.d.ts +36 -0
- package/dist/catalogue/canonicalisation.js +262 -0
- package/dist/catalogue/config.d.ts +13 -0
- package/dist/catalogue/config.js +77 -0
- package/dist/catalogue/deterministic.d.ts +3 -0
- package/dist/catalogue/deterministic.js +25 -0
- package/dist/catalogue/diagnostics.d.ts +2 -0
- package/dist/catalogue/diagnostics.js +34 -0
- package/dist/catalogue/evidence.d.ts +1 -0
- package/dist/catalogue/evidence.js +10 -0
- package/dist/catalogue/examples.d.ts +3 -0
- package/dist/catalogue/examples.js +20 -0
- package/dist/catalogue/identity.d.ts +61 -0
- package/dist/catalogue/identity.js +114 -0
- package/dist/catalogue/ignore.d.ts +4 -0
- package/dist/catalogue/ignore.js +29 -0
- package/dist/catalogue/indexer.d.ts +20 -0
- package/dist/catalogue/indexer.js +1393 -0
- package/dist/catalogue/linkage.d.ts +16 -0
- package/dist/catalogue/linkage.js +419 -0
- package/dist/catalogue/moduleResolution.d.ts +4 -0
- package/dist/catalogue/moduleResolution.js +67 -0
- package/dist/catalogue/ordering.d.ts +13 -0
- package/dist/catalogue/ordering.js +86 -0
- package/dist/catalogue/packages.d.ts +12 -0
- package/dist/catalogue/packages.js +475 -0
- package/dist/catalogue/pythonReExports.d.ts +7 -0
- package/dist/catalogue/pythonReExports.js +113 -0
- package/dist/catalogue/resolveRefs.d.ts +40 -0
- package/dist/catalogue/resolveRefs.js +284 -0
- package/dist/catalogue/scanner.d.ts +8 -0
- package/dist/catalogue/scanner.js +99 -0
- package/dist/catalogue/schema.d.ts +289 -0
- package/dist/catalogue/schema.js +1 -0
- package/dist/catalogue/store.d.ts +41 -0
- package/dist/catalogue/store.js +209 -0
- package/dist/catalogue/templates.d.ts +18 -0
- package/dist/catalogue/templates.js +158 -0
- package/dist/catalogue/version.d.ts +12 -0
- package/dist/catalogue/version.js +30 -0
- package/dist/cli/commands/adoptionPlan.d.ts +8 -0
- package/dist/cli/commands/adoptionPlan.js +24 -0
- package/dist/cli/commands/auditDocs.d.ts +8 -0
- package/dist/cli/commands/auditDocs.js +24 -0
- package/dist/cli/commands/changeImpact.d.ts +10 -0
- package/dist/cli/commands/changeImpact.js +36 -0
- package/dist/cli/commands/changes.d.ts +12 -0
- package/dist/cli/commands/changes.js +31 -0
- package/dist/cli/commands/check.d.ts +9 -0
- package/dist/cli/commands/check.js +4 -0
- package/dist/cli/commands/compatibility.d.ts +10 -0
- package/dist/cli/commands/compatibility.js +43 -0
- package/dist/cli/commands/diff.d.ts +31 -0
- package/dist/cli/commands/diff.js +118 -0
- package/dist/cli/commands/dogfood.d.ts +8 -0
- package/dist/cli/commands/dogfood.js +24 -0
- package/dist/cli/commands/drift.d.ts +8 -0
- package/dist/cli/commands/drift.js +13 -0
- package/dist/cli/commands/index.d.ts +9 -0
- package/dist/cli/commands/index.js +9 -0
- package/dist/cli/commands/indexWorkspace.d.ts +8 -0
- package/dist/cli/commands/indexWorkspace.js +23 -0
- package/dist/cli/commands/recommendConfig.d.ts +7 -0
- package/dist/cli/commands/recommendConfig.js +20 -0
- package/dist/cli/commands/schema.d.ts +1 -0
- package/dist/cli/commands/schema.js +37 -0
- package/dist/cli/commands/template.d.ts +5 -0
- package/dist/cli/commands/template.js +44 -0
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.js +85 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +211 -0
- package/dist/comparison/artefactValidate.d.ts +19 -0
- package/dist/comparison/artefactValidate.js +334 -0
- package/dist/comparison/candidates.d.ts +12 -0
- package/dist/comparison/candidates.js +391 -0
- package/dist/comparison/compare.d.ts +15 -0
- package/dist/comparison/compare.js +240 -0
- package/dist/comparison/compatibility.d.ts +26 -0
- package/dist/comparison/compatibility.js +316 -0
- package/dist/comparison/drift.d.ts +34 -0
- package/dist/comparison/drift.js +71 -0
- package/dist/comparison/identity.d.ts +30 -0
- package/dist/comparison/identity.js +73 -0
- package/dist/comparison/impact.d.ts +14 -0
- package/dist/comparison/impact.js +291 -0
- package/dist/comparison/index.d.ts +6 -0
- package/dist/comparison/index.js +6 -0
- package/dist/comparison/integrity.d.ts +12 -0
- package/dist/comparison/integrity.js +199 -0
- package/dist/comparison/ordering.d.ts +4 -0
- package/dist/comparison/ordering.js +69 -0
- package/dist/comparison/projections.d.ts +19 -0
- package/dist/comparison/projections.js +185 -0
- package/dist/comparison/relationshipPolicy.d.ts +19 -0
- package/dist/comparison/relationshipPolicy.js +98 -0
- package/dist/comparison/schema.d.ts +178 -0
- package/dist/comparison/schema.js +16 -0
- package/dist/comparison/store.d.ts +58 -0
- package/dist/comparison/store.js +462 -0
- package/dist/comparison/validate.d.ts +23 -0
- package/dist/comparison/validate.js +137 -0
- package/dist/comparison/version.d.ts +9 -0
- package/dist/comparison/version.js +11 -0
- package/dist/extractors/docTags.d.ts +9 -0
- package/dist/extractors/docTags.js +106 -0
- package/dist/extractors/generic.d.ts +5 -0
- package/dist/extractors/generic.js +3 -0
- package/dist/extractors/javascript.d.ts +3 -0
- package/dist/extractors/javascript.js +4 -0
- package/dist/extractors/markdown.d.ts +7 -0
- package/dist/extractors/markdown.js +269 -0
- package/dist/extractors/python.d.ts +7 -0
- package/dist/extractors/python.js +314 -0
- package/dist/extractors/typescript.d.ts +7 -0
- package/dist/extractors/typescript.js +387 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/resources.d.ts +18 -0
- package/dist/mcp/resources.js +57 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +95 -0
- package/dist/mcp/tools.d.ts +29 -0
- package/dist/mcp/tools.js +902 -0
- package/dist/queries/adoption.d.ts +62 -0
- package/dist/queries/adoption.js +366 -0
- package/dist/queries/capabilities.d.ts +45 -0
- package/dist/queries/capabilities.js +124 -0
- package/dist/queries/changes.d.ts +33 -0
- package/dist/queries/changes.js +53 -0
- package/dist/queries/dependencies.d.ts +17 -0
- package/dist/queries/dependencies.js +94 -0
- package/dist/queries/dogfood.d.ts +50 -0
- package/dist/queries/dogfood.js +116 -0
- package/dist/queries/editingContext.d.ts +84 -0
- package/dist/queries/editingContext.js +240 -0
- package/dist/queries/examples.d.ts +8 -0
- package/dist/queries/examples.js +16 -0
- package/dist/queries/files.d.ts +8 -0
- package/dist/queries/files.js +14 -0
- package/dist/queries/lists.d.ts +94 -0
- package/dist/queries/lists.js +184 -0
- package/dist/queries/overview.d.ts +2 -0
- package/dist/queries/overview.js +8 -0
- package/dist/queries/publicApis.d.ts +60 -0
- package/dist/queries/publicApis.js +120 -0
- package/dist/queries/ranking.d.ts +67 -0
- package/dist/queries/ranking.js +387 -0
- package/dist/queries/recommendConfig.d.ts +47 -0
- package/dist/queries/recommendConfig.js +123 -0
- package/dist/queries/relationships.d.ts +5 -0
- package/dist/queries/relationships.js +15 -0
- package/dist/queries/reuseCandidates.d.ts +34 -0
- package/dist/queries/reuseCandidates.js +181 -0
- package/dist/queries/symbols.d.ts +40 -0
- package/dist/queries/symbols.js +41 -0
- package/dist/security/pathPolicy.d.ts +12 -0
- package/dist/security/pathPolicy.js +46 -0
- package/dist/security/secretPatterns.d.ts +1 -0
- package/dist/security/secretPatterns.js +4 -0
- package/dist/security/sourceAccess.d.ts +12 -0
- package/dist/security/sourceAccess.js +35 -0
- package/package.json +60 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { toPosixPath } from "./moduleResolution.js";
|
|
4
|
+
import { packageIdentityId } from "./identity.js";
|
|
5
|
+
export function buildPackageModel(workspaceRoot, files, markdownPackages) {
|
|
6
|
+
const packageByPath = new Map();
|
|
7
|
+
const entrypointImportPathByFile = new Map();
|
|
8
|
+
for (const packageRecord of markdownPackages) {
|
|
9
|
+
packageByPath.set(normalizePackagePath(packageRecord.path), clonePackageRecord(packageRecord));
|
|
10
|
+
}
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
const absolute = path.resolve(workspaceRoot, file.path);
|
|
13
|
+
const baseName = path.basename(file.path).toLowerCase();
|
|
14
|
+
if (baseName === "package.json") {
|
|
15
|
+
const packageRoot = normalizePackagePath(path.dirname(file.path));
|
|
16
|
+
const packageJson = readJsonIfExists(absolute);
|
|
17
|
+
const name = typeof packageJson?.name === "string" ? packageJson.name : undefined;
|
|
18
|
+
const entrypointInfos = collectPackageJsonEntrypoints(workspaceRoot, packageRoot, packageJson);
|
|
19
|
+
for (const entrypoint of entrypointInfos) {
|
|
20
|
+
if (entrypoint.importPath) {
|
|
21
|
+
entrypointImportPathByFile.set(entrypoint.filePath, entrypoint.importPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
upsertPackage(packageByPath, packageRoot, {
|
|
25
|
+
name,
|
|
26
|
+
ecosystem: "npm",
|
|
27
|
+
summary: undefined,
|
|
28
|
+
entrypoints: entrypointInfos.map((entrypoint) => entrypoint.filePath),
|
|
29
|
+
provenanceWarnings: isPrivateWorkspaceRootPackageJson(packageRoot, packageJson)
|
|
30
|
+
? ["private-workspace-root"]
|
|
31
|
+
: []
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (baseName === "pyproject.toml") {
|
|
36
|
+
const packageRoot = normalizePackagePath(path.dirname(file.path));
|
|
37
|
+
const name = readPyprojectName(absolute);
|
|
38
|
+
const pythonMeta = readPyprojectMetadata(absolute);
|
|
39
|
+
const entrypointInfos = collectPythonEntrypointInfos(workspaceRoot, packageRoot, files, pythonMeta.importPackages);
|
|
40
|
+
for (const entrypoint of entrypointInfos) {
|
|
41
|
+
if (entrypoint.importPath) {
|
|
42
|
+
entrypointImportPathByFile.set(entrypoint.filePath, entrypoint.importPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
upsertPackage(packageByPath, packageRoot, {
|
|
46
|
+
name,
|
|
47
|
+
ecosystem: "python",
|
|
48
|
+
summary: undefined,
|
|
49
|
+
entrypoints: entrypointInfos.map((entrypoint) => entrypoint.filePath),
|
|
50
|
+
importPackages: pythonMeta.importPackages,
|
|
51
|
+
consoleScripts: pythonMeta.consoleScripts
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (/CATALOGUE\.md$/i.test(file.path)) {
|
|
56
|
+
const packageRoot = normalizePackagePath(path.dirname(file.path));
|
|
57
|
+
const existing = packageByPath.get(packageRoot);
|
|
58
|
+
if (!existing) {
|
|
59
|
+
const catalogueName = readCataloguePackageName(absolute);
|
|
60
|
+
upsertPackage(packageByPath, packageRoot, {
|
|
61
|
+
name: catalogueName,
|
|
62
|
+
summary: readCatalogueSummary(absolute)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
hydrateRootPackageFromWorkspaceManifest(workspaceRoot, files, packageByPath, entrypointImportPathByFile);
|
|
68
|
+
if (packageByPath.size === 0) {
|
|
69
|
+
const fallbackName = path.basename(path.resolve(workspaceRoot)) || "workspace";
|
|
70
|
+
upsertPackage(packageByPath, ".", { name: fallbackName });
|
|
71
|
+
}
|
|
72
|
+
else if (!packageByPath.has(".")) {
|
|
73
|
+
const fallbackName = path.basename(path.resolve(workspaceRoot)) || "workspace";
|
|
74
|
+
upsertPackage(packageByPath, ".", { name: fallbackName });
|
|
75
|
+
}
|
|
76
|
+
const orderedPackages = [...packageByPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
77
|
+
const packageById = new Map();
|
|
78
|
+
for (const packageRecord of orderedPackages) {
|
|
79
|
+
packageById.set(packageRecord.id, packageRecord);
|
|
80
|
+
}
|
|
81
|
+
const sortedPackageRoots = orderedPackages
|
|
82
|
+
.map((packageRecord) => packageRecord.path)
|
|
83
|
+
.sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
84
|
+
const filePackageByPath = new Map();
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const packageRecord = findPackageForPath(sortedPackageRoots, packageByPath, file.path);
|
|
87
|
+
if (packageRecord) {
|
|
88
|
+
file.packageName = packageRecord.name;
|
|
89
|
+
filePackageByPath.set(file.path, packageRecord);
|
|
90
|
+
if (file.kind === "source" && !packageRecord.modules?.includes(file.path)) {
|
|
91
|
+
packageRecord.modules = [...new Set([...(packageRecord.modules ?? []), file.path])].sort();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const packageRecord of orderedPackages) {
|
|
96
|
+
packageRecord.modules = [...new Set(packageRecord.modules ?? [])].sort();
|
|
97
|
+
packageRecord.entrypoints = [...new Set(packageRecord.entrypoints ?? [])].sort();
|
|
98
|
+
packageRecord.capabilities = [...new Set(packageRecord.capabilities)].sort();
|
|
99
|
+
packageRecord.publicApis = [...new Set(packageRecord.publicApis)].sort();
|
|
100
|
+
packageRecord.tests = [...new Set(packageRecord.tests)].sort();
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
packages: orderedPackages,
|
|
104
|
+
packageByPath,
|
|
105
|
+
packageById,
|
|
106
|
+
filePackageByPath,
|
|
107
|
+
entrypointImportPathByFile
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function findPackageForFile(model, filePath) {
|
|
111
|
+
return model.filePackageByPath.get(filePath);
|
|
112
|
+
}
|
|
113
|
+
export function computePublicApiImportPath(workspaceRoot, filePath, packageRecord, language, publicImportPath) {
|
|
114
|
+
const normalizedFilePath = toPosixPath(filePath);
|
|
115
|
+
const packagePath = packageRecord?.path ?? ".";
|
|
116
|
+
const packageName = packageRecord?.name;
|
|
117
|
+
if (language === "typescript" || language === "javascript") {
|
|
118
|
+
if (publicImportPath) {
|
|
119
|
+
return publicImportPath;
|
|
120
|
+
}
|
|
121
|
+
if (packageName && packageRecord?.entrypoints?.some((entrypoint) => matchesEntrypoint(filePath, entrypoint))) {
|
|
122
|
+
return packageName;
|
|
123
|
+
}
|
|
124
|
+
return stripExtension(normalizedFilePath);
|
|
125
|
+
}
|
|
126
|
+
if (language === "python") {
|
|
127
|
+
// Prefer a package-root import path attributed via explicit __init__ re-exports.
|
|
128
|
+
if (publicImportPath) {
|
|
129
|
+
return publicImportPath;
|
|
130
|
+
}
|
|
131
|
+
const packageRoot = packagePath === "." ? workspaceRoot : path.resolve(workspaceRoot, packagePath);
|
|
132
|
+
const relative = path.relative(packageRoot, path.resolve(workspaceRoot, filePath));
|
|
133
|
+
return stripPythonModulePath(toPosixPath(relative));
|
|
134
|
+
}
|
|
135
|
+
return normalizedFilePath;
|
|
136
|
+
}
|
|
137
|
+
export function buildPublicImportPathBySymbolId(packageModel, relationships) {
|
|
138
|
+
const exportRelationshipsByFile = new Map();
|
|
139
|
+
for (const relationship of relationships) {
|
|
140
|
+
if (relationship.type !== "exports" || relationship.from.kind !== "file") {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const current = exportRelationshipsByFile.get(relationship.from.id) ?? [];
|
|
144
|
+
current.push(relationship);
|
|
145
|
+
exportRelationshipsByFile.set(relationship.from.id, current);
|
|
146
|
+
}
|
|
147
|
+
const result = new Map();
|
|
148
|
+
for (const packageRecord of packageModel.packages) {
|
|
149
|
+
if (!packageRecord.name) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const entrypoint of packageRecord.entrypoints ?? []) {
|
|
153
|
+
const publicImportPath = packageModel.entrypointImportPathByFile.get(entrypoint);
|
|
154
|
+
if (!publicImportPath) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const queue = [entrypoint];
|
|
158
|
+
const visitedFiles = new Set();
|
|
159
|
+
while (queue.length > 0) {
|
|
160
|
+
const filePath = queue.shift();
|
|
161
|
+
if (visitedFiles.has(filePath)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
visitedFiles.add(filePath);
|
|
165
|
+
const exportRelationships = exportRelationshipsByFile.get(filePath) ?? [];
|
|
166
|
+
for (const relationship of exportRelationships) {
|
|
167
|
+
if (relationship.to.kind === "symbol") {
|
|
168
|
+
result.set(relationship.to.id, result.get(relationship.to.id) ?? publicImportPath);
|
|
169
|
+
}
|
|
170
|
+
else if (relationship.to.kind === "file") {
|
|
171
|
+
queue.push(relationship.to.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function upsertPackage(packageByPath, packagePath, seed) {
|
|
180
|
+
const existing = packageByPath.get(packagePath);
|
|
181
|
+
const pathValue = normalizePackagePath(packagePath);
|
|
182
|
+
const ecosystem = seed.ecosystem ?? existing?.ecosystem ?? "unknown";
|
|
183
|
+
if (existing) {
|
|
184
|
+
if (seed.name) {
|
|
185
|
+
existing.name = seed.name;
|
|
186
|
+
}
|
|
187
|
+
existing.ecosystem = ecosystem;
|
|
188
|
+
const identityName = existing.name && existing.name !== pathValue ? existing.name : seed.name ?? "unnamed";
|
|
189
|
+
if (!existing.name || existing.name === pathValue) {
|
|
190
|
+
existing.name = seed.name ?? "unnamed";
|
|
191
|
+
}
|
|
192
|
+
existing.id = packageIdentityId({ ecosystem, name: identityName === pathValue ? "unnamed" : identityName, root: pathValue });
|
|
193
|
+
existing.summary = existing.summary ?? seed.summary;
|
|
194
|
+
existing.entrypoints = [...new Set([...(existing.entrypoints ?? []), ...(seed.entrypoints ?? [])])];
|
|
195
|
+
if (seed.importPackages) {
|
|
196
|
+
existing.importPackages = [...new Set([...(existing.importPackages ?? []), ...seed.importPackages])].sort();
|
|
197
|
+
}
|
|
198
|
+
if (seed.consoleScripts) {
|
|
199
|
+
existing.consoleScripts = dedupeConsoleScripts([...(existing.consoleScripts ?? []), ...seed.consoleScripts]);
|
|
200
|
+
}
|
|
201
|
+
if ((seed.provenanceWarnings?.length ?? 0) > 0) {
|
|
202
|
+
existing.provenance.warnings = [...new Set([...(existing.provenance.warnings ?? []), ...seed.provenanceWarnings])].sort();
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const identityName = seed.name ?? "unnamed";
|
|
207
|
+
packageByPath.set(packagePath, {
|
|
208
|
+
id: packageIdentityId({ ecosystem, name: identityName, root: pathValue }),
|
|
209
|
+
name: identityName,
|
|
210
|
+
path: pathValue,
|
|
211
|
+
ecosystem,
|
|
212
|
+
importPackages: seed.importPackages ? [...seed.importPackages].sort() : undefined,
|
|
213
|
+
consoleScripts: seed.consoleScripts ? dedupeConsoleScripts(seed.consoleScripts) : undefined,
|
|
214
|
+
summary: seed.summary,
|
|
215
|
+
capabilities: [],
|
|
216
|
+
publicApis: [],
|
|
217
|
+
tests: [],
|
|
218
|
+
stability: "unknown",
|
|
219
|
+
entrypoints: [...new Set(seed.entrypoints ?? [])],
|
|
220
|
+
modules: [],
|
|
221
|
+
provenance: {
|
|
222
|
+
source: "package-model",
|
|
223
|
+
warnings: [...new Set(seed.provenanceWarnings ?? [])].sort()
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function dedupeConsoleScripts(scripts) {
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const script of scripts) {
|
|
231
|
+
const key = `${script.name}\0${script.target}`;
|
|
232
|
+
if (seen.has(key))
|
|
233
|
+
continue;
|
|
234
|
+
seen.add(key);
|
|
235
|
+
out.push(script);
|
|
236
|
+
}
|
|
237
|
+
return out.sort((a, b) => a.name.localeCompare(b.name) || a.target.localeCompare(b.target));
|
|
238
|
+
}
|
|
239
|
+
function findPackageForPath(sortedPackageRoots, packageByPath, filePath) {
|
|
240
|
+
for (const packageRoot of sortedPackageRoots) {
|
|
241
|
+
if (packageRoot === ".") {
|
|
242
|
+
return packageByPath.get(packageRoot);
|
|
243
|
+
}
|
|
244
|
+
if (filePath === packageRoot || filePath.startsWith(`${packageRoot}/`)) {
|
|
245
|
+
return packageByPath.get(packageRoot);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return packageByPath.get(".");
|
|
249
|
+
}
|
|
250
|
+
function normalizePackagePath(packagePath) {
|
|
251
|
+
const normalized = packagePath.replaceAll("\\", "/");
|
|
252
|
+
return normalized === "" ? "." : normalized;
|
|
253
|
+
}
|
|
254
|
+
function clonePackageRecord(packageRecord) {
|
|
255
|
+
return {
|
|
256
|
+
...packageRecord,
|
|
257
|
+
capabilities: [...packageRecord.capabilities],
|
|
258
|
+
publicApis: [...packageRecord.publicApis],
|
|
259
|
+
tests: [...packageRecord.tests],
|
|
260
|
+
entrypoints: [...(packageRecord.entrypoints ?? [])],
|
|
261
|
+
modules: [...(packageRecord.modules ?? [])],
|
|
262
|
+
internalPaths: [...(packageRecord.internalPaths ?? [])],
|
|
263
|
+
publicPaths: [...(packageRecord.publicPaths ?? [])],
|
|
264
|
+
dependencyPolicy: packageRecord.dependencyPolicy ? { ...packageRecord.dependencyPolicy } : undefined,
|
|
265
|
+
provenance: {
|
|
266
|
+
...packageRecord.provenance,
|
|
267
|
+
extractors: [...(packageRecord.provenance.extractors ?? [])],
|
|
268
|
+
locations: [...(packageRecord.provenance.locations ?? [])],
|
|
269
|
+
warnings: [...(packageRecord.provenance.warnings ?? [])]
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function readJsonIfExists(filePath) {
|
|
274
|
+
if (!existsSync(filePath)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function readPyprojectName(filePath) {
|
|
285
|
+
if (!existsSync(filePath)) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const text = readFileSync(filePath, "utf8");
|
|
289
|
+
const projectMatch = text.match(/(?:\[project\]|\[tool\.poetry\])([\s\S]*?)(?:\n\[|$)/);
|
|
290
|
+
const body = projectMatch?.[1] ?? text;
|
|
291
|
+
const nameMatch = body.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
292
|
+
return nameMatch?.[1];
|
|
293
|
+
}
|
|
294
|
+
function readPyprojectMetadata(filePath) {
|
|
295
|
+
const importPackages = [];
|
|
296
|
+
const consoleScripts = [];
|
|
297
|
+
if (!existsSync(filePath)) {
|
|
298
|
+
return { importPackages, consoleScripts };
|
|
299
|
+
}
|
|
300
|
+
const text = readFileSync(filePath, "utf8");
|
|
301
|
+
const scriptsMatch = text.match(/\[project\.scripts\]([\s\S]*?)(?:\n\[|$)/);
|
|
302
|
+
if (scriptsMatch?.[1]) {
|
|
303
|
+
for (const line of scriptsMatch[1].split(/\r?\n/)) {
|
|
304
|
+
const m = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*["']([^"']+)["']/);
|
|
305
|
+
if (m) {
|
|
306
|
+
consoleScripts.push({ name: m[1], target: m[2] });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Prefer packages listed under [tool.setuptools.packages] find where present; otherwise leave empty for layout inference later.
|
|
311
|
+
const packagesList = text.match(/packages\s*=\s*\[\s*((?:"[^"]+"\s*,?\s*)+)\s*\]/);
|
|
312
|
+
if (packagesList?.[1]) {
|
|
313
|
+
for (const m of packagesList[1].matchAll(/"([^"]+)"/g)) {
|
|
314
|
+
importPackages.push(m[1]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { importPackages: [...new Set(importPackages)].sort(), consoleScripts };
|
|
318
|
+
}
|
|
319
|
+
function readCataloguePackageName(filePath) {
|
|
320
|
+
const text = readFileSync(filePath, "utf8");
|
|
321
|
+
const match = text.match(/^Name:\s*(.+)$/m);
|
|
322
|
+
return match?.[1]?.replace(/`/g, "").trim();
|
|
323
|
+
}
|
|
324
|
+
function readCatalogueSummary(filePath) {
|
|
325
|
+
const text = readFileSync(filePath, "utf8");
|
|
326
|
+
const match = text.match(/^Purpose:\s*([\s\S]*?)(?:\n\n|$)/m);
|
|
327
|
+
return match?.[1]?.trim();
|
|
328
|
+
}
|
|
329
|
+
function collectPackageJsonEntrypoints(workspaceRoot, packageRoot, packageJson) {
|
|
330
|
+
const entrypoints = [];
|
|
331
|
+
if (!packageJson) {
|
|
332
|
+
return entrypoints;
|
|
333
|
+
}
|
|
334
|
+
const packageName = typeof packageJson.name === "string" ? packageJson.name : undefined;
|
|
335
|
+
const candidates = [
|
|
336
|
+
{ value: packageJson.main, importPath: packageName },
|
|
337
|
+
{ value: packageJson.module, importPath: packageName }
|
|
338
|
+
];
|
|
339
|
+
if (typeof packageJson.exports === "string") {
|
|
340
|
+
candidates.push({ value: packageJson.exports, importPath: packageName });
|
|
341
|
+
}
|
|
342
|
+
if (packageJson.exports && typeof packageJson.exports === "object" && !Array.isArray(packageJson.exports)) {
|
|
343
|
+
const exportsMap = packageJson.exports;
|
|
344
|
+
const rootExport = exportsMap["."];
|
|
345
|
+
for (const value of collectExportTargetStrings(rootExport)) {
|
|
346
|
+
candidates.push({ value, importPath: packageName });
|
|
347
|
+
}
|
|
348
|
+
for (const [subpath, target] of Object.entries(exportsMap)) {
|
|
349
|
+
if (!subpath.startsWith("./") || subpath === ".") {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const normalizedSubpath = subpath.slice(2);
|
|
353
|
+
const importPath = packageName && normalizedSubpath ? `${packageName}/${normalizedSubpath}` : undefined;
|
|
354
|
+
for (const value of collectExportTargetStrings(target)) {
|
|
355
|
+
candidates.push({ value, importPath });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const candidate of candidates) {
|
|
360
|
+
if (typeof candidate.value !== "string") {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const normalized = toPosixPath(path.join(packageRoot, candidate.value));
|
|
364
|
+
entrypoints.push({ filePath: normalized, importPath: candidate.importPath });
|
|
365
|
+
}
|
|
366
|
+
for (const localCandidate of ["index.ts", "index.tsx", "index.js", "index.jsx", "index.py", path.join("src", "index.ts"), path.join("src", "index.tsx"), path.join("src", "index.js"), path.join("src", "index.jsx"), path.join("src", "__init__.py")]) {
|
|
367
|
+
const relativePath = toPosixPath(path.join(packageRoot, localCandidate));
|
|
368
|
+
if (existsSync(path.resolve(workspaceRoot, relativePath))) {
|
|
369
|
+
entrypoints.push({ filePath: relativePath, importPath: packageName });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const seen = new Set();
|
|
373
|
+
return entrypoints.filter((entrypoint) => {
|
|
374
|
+
const key = `${entrypoint.filePath}\0${entrypoint.importPath ?? ""}`;
|
|
375
|
+
if (seen.has(key)) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
seen.add(key);
|
|
379
|
+
return true;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function collectExportTargetStrings(target) {
|
|
383
|
+
if (typeof target === "string") {
|
|
384
|
+
return [target];
|
|
385
|
+
}
|
|
386
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const preferredKeys = ["types", "import", "require", "default"];
|
|
390
|
+
const record = target;
|
|
391
|
+
const values = preferredKeys.flatMap((key) => collectExportTargetStrings(record[key]));
|
|
392
|
+
return [...new Set(values)];
|
|
393
|
+
}
|
|
394
|
+
function isPrivateWorkspaceRootPackageJson(packageRoot, packageJson) {
|
|
395
|
+
if (packageRoot !== "." || !packageJson) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const isPrivate = packageJson.private === true;
|
|
399
|
+
const hasWorkspaces = Array.isArray(packageJson.workspaces)
|
|
400
|
+
|| (typeof packageJson.workspaces === "object" && packageJson.workspaces !== null);
|
|
401
|
+
return isPrivate && hasWorkspaces;
|
|
402
|
+
}
|
|
403
|
+
function hydrateRootPackageFromWorkspaceManifest(workspaceRoot, files, packageByPath, entrypointImportPathByFile) {
|
|
404
|
+
if (packageByPath.has(".")) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const rootPackageJsonPath = path.resolve(workspaceRoot, "package.json");
|
|
408
|
+
if (!existsSync(rootPackageJsonPath)) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const packageJson = readJsonIfExists(rootPackageJsonPath);
|
|
412
|
+
const name = typeof packageJson?.name === "string" ? packageJson.name : undefined;
|
|
413
|
+
const entrypoints = collectPackageJsonEntrypoints(workspaceRoot, ".", packageJson);
|
|
414
|
+
for (const entrypoint of entrypoints) {
|
|
415
|
+
if (entrypoint.importPath) {
|
|
416
|
+
entrypointImportPathByFile.set(entrypoint.filePath, entrypoint.importPath);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const hasRootPackageFile = files.some((file) => file.path === "package.json");
|
|
420
|
+
upsertPackage(packageByPath, ".", {
|
|
421
|
+
name,
|
|
422
|
+
ecosystem: "npm",
|
|
423
|
+
summary: undefined,
|
|
424
|
+
entrypoints: entrypoints.map((entrypoint) => entrypoint.filePath),
|
|
425
|
+
provenanceWarnings: isPrivateWorkspaceRootPackageJson(".", packageJson)
|
|
426
|
+
? [hasRootPackageFile ? "private-workspace-root" : "private-workspace-root-manifest"]
|
|
427
|
+
: []
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function collectPythonEntrypointInfos(workspaceRoot, packageRoot, files, importPackages) {
|
|
431
|
+
const infos = [];
|
|
432
|
+
// Legacy package/src root __init__.py (import path not statically known).
|
|
433
|
+
for (const file of files) {
|
|
434
|
+
if ((packageRoot === "." && (file.path === "__init__.py" || file.path === "src/__init__.py")) ||
|
|
435
|
+
file.path === `${packageRoot}/__init__.py` ||
|
|
436
|
+
file.path === `${packageRoot}/src/__init__.py`) {
|
|
437
|
+
infos.push({ filePath: file.path });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Import packages declared in pyproject map their `__init__.py` to a dotted
|
|
441
|
+
// import path (distribution name may differ from the import package name).
|
|
442
|
+
for (const importPackage of importPackages) {
|
|
443
|
+
const relativeDir = importPackage.replaceAll(".", "/");
|
|
444
|
+
const candidates = packageRoot === "."
|
|
445
|
+
? [`${relativeDir}/__init__.py`, `src/${relativeDir}/__init__.py`]
|
|
446
|
+
: [`${packageRoot}/${relativeDir}/__init__.py`, `${packageRoot}/src/${relativeDir}/__init__.py`];
|
|
447
|
+
for (const candidate of candidates) {
|
|
448
|
+
const normalized = toPosixPath(candidate);
|
|
449
|
+
if (existsSync(path.resolve(workspaceRoot, normalized))) {
|
|
450
|
+
infos.push({ filePath: normalized, importPath: importPackage });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const seen = new Set();
|
|
455
|
+
return infos.filter((info) => {
|
|
456
|
+
const key = `${info.filePath}\0${info.importPath ?? ""}`;
|
|
457
|
+
if (seen.has(key)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
seen.add(key);
|
|
461
|
+
return true;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function matchesEntrypoint(filePath, entrypoint) {
|
|
465
|
+
const normalizedEntry = toPosixPath(entrypoint);
|
|
466
|
+
return filePath === normalizedEntry || filePath.endsWith(`/${path.basename(normalizedEntry)}`);
|
|
467
|
+
}
|
|
468
|
+
function stripExtension(filePath) {
|
|
469
|
+
return filePath.replace(/\.[^.\/]+$/, "");
|
|
470
|
+
}
|
|
471
|
+
function stripPythonModulePath(modulePath) {
|
|
472
|
+
const stripped = modulePath.replace(/\.py$/, "").replace(/\/__init__$/, "");
|
|
473
|
+
const dotted = stripped.replaceAll("/", ".");
|
|
474
|
+
return dotted === "." ? "" : dotted;
|
|
475
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileRecord, RelationshipRecord, SymbolRecord } from "./schema.js";
|
|
2
|
+
export interface PythonReExportInput {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
files: FileRecord[];
|
|
5
|
+
symbols: SymbolRecord[];
|
|
6
|
+
}
|
|
7
|
+
export declare function buildPythonInitReExports(input: PythonReExportInput): RelationshipRecord[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate 3 Python public-surface: explicit `__init__.py` re-exports.
|
|
3
|
+
*
|
|
4
|
+
* A statement like `from .core import Greeter` inside a package `__init__.py`
|
|
5
|
+
* makes `Greeter` importable from the package itself. We model this with an
|
|
6
|
+
* `exports` relationship (file → symbol, discriminant `re-export`) so that the
|
|
7
|
+
* public import-path resolver can attribute the package root import path to the
|
|
8
|
+
* re-exported symbol. Only statically clear relative imports of concrete names
|
|
9
|
+
* are recognised; wildcard (`*`) and dynamic constructs are ignored.
|
|
10
|
+
*/
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { relationshipIdentityId } from "./identity.js";
|
|
14
|
+
import { truncateEvidenceText } from "./evidence.js";
|
|
15
|
+
import { isGeneratedArtefactPath } from "./resolveRefs.js";
|
|
16
|
+
import { resolvePythonRelativeModuleTarget } from "./moduleResolution.js";
|
|
17
|
+
export function buildPythonInitReExports(input) {
|
|
18
|
+
const topLevelByFileName = new Map();
|
|
19
|
+
for (const symbol of input.symbols) {
|
|
20
|
+
if (symbol.language !== "python" || symbol.duplicateOf) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if ((symbol.qualifiedContainerPath ?? "") !== "") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const key = `${symbol.filePath}\0${symbol.name}`;
|
|
27
|
+
const current = topLevelByFileName.get(key) ?? [];
|
|
28
|
+
current.push(symbol.id);
|
|
29
|
+
topLevelByFileName.set(key, current);
|
|
30
|
+
}
|
|
31
|
+
const symbolById = new Map(input.symbols.map((symbol) => [symbol.id, symbol]));
|
|
32
|
+
const merged = new Map();
|
|
33
|
+
for (const file of input.files) {
|
|
34
|
+
if (file.language !== "python" || path.basename(file.path) !== "__init__.py") {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const absolute = path.resolve(input.workspaceRoot, file.path);
|
|
38
|
+
let lines;
|
|
39
|
+
try {
|
|
40
|
+
lines = readFileSync(absolute, "utf8").split(/\r?\n/);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
46
|
+
const line = lines[i] ?? "";
|
|
47
|
+
const match = line.match(/^\s*from\s+(\.[.A-Za-z_0-9]*)\s+import\s+(.+)$/);
|
|
48
|
+
if (!match) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const moduleSpecifier = match[1]?.trim() ?? "";
|
|
52
|
+
const targetFile = resolvePythonRelativeModuleTarget(input.workspaceRoot, file.path, moduleSpecifier);
|
|
53
|
+
if (!targetFile) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const importList = (match[2] ?? "").replace(/[()\\]/g, " ");
|
|
57
|
+
for (const rawEntry of importList.split(",")) {
|
|
58
|
+
const entry = rawEntry.trim();
|
|
59
|
+
if (!entry || entry === "*") {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const externalName = entry.split(/\s+as\s+/i)[0]?.trim();
|
|
63
|
+
if (!externalName || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(externalName)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const candidates = topLevelByFileName.get(`${targetFile}\0${externalName}`) ?? [];
|
|
67
|
+
const symbolId = preferSourceId(candidates, symbolById);
|
|
68
|
+
if (!symbolId) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const id = relationshipIdentityId({
|
|
72
|
+
type: "exports",
|
|
73
|
+
fromKind: "file",
|
|
74
|
+
fromId: file.path,
|
|
75
|
+
toKind: "symbol",
|
|
76
|
+
toId: symbolId,
|
|
77
|
+
discriminant: "re-export"
|
|
78
|
+
});
|
|
79
|
+
if (merged.has(id)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
merged.set(id, {
|
|
83
|
+
id,
|
|
84
|
+
type: "exports",
|
|
85
|
+
from: { kind: "file", id: file.path },
|
|
86
|
+
to: { kind: "symbol", id: symbolId },
|
|
87
|
+
localDiscriminant: "re-export",
|
|
88
|
+
evidence: {
|
|
89
|
+
filePath: file.path,
|
|
90
|
+
startLine: i + 1,
|
|
91
|
+
endLine: i + 1,
|
|
92
|
+
text: truncateEvidenceText(line.trim())
|
|
93
|
+
},
|
|
94
|
+
confidence: 0.9,
|
|
95
|
+
provenance: { source: "python-static", extractors: ["python-init"] }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
101
|
+
}
|
|
102
|
+
function preferSourceId(ids, symbolById) {
|
|
103
|
+
if (ids.length === 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const unique = [...new Set(ids)];
|
|
107
|
+
const authored = unique.filter((id) => {
|
|
108
|
+
const filePath = symbolById.get(id)?.filePath;
|
|
109
|
+
return filePath ? !isGeneratedArtefactPath(filePath) : true;
|
|
110
|
+
});
|
|
111
|
+
const pool = authored.length > 0 ? authored : unique;
|
|
112
|
+
return [...pool].sort()[0];
|
|
113
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PackageRecord, PublicApiRecord, SymbolRecord } from "./schema.js";
|
|
2
|
+
export type RefResolutionStatus = "resolved" | "ambiguous" | "unknown";
|
|
3
|
+
export interface RefResolution {
|
|
4
|
+
status: RefResolutionStatus;
|
|
5
|
+
/** Canonical id when resolved; otherwise the original reference. */
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SymbolRefIndex {
|
|
9
|
+
byId: Map<string, SymbolRecord>;
|
|
10
|
+
byQualifiedName: Map<string, string>;
|
|
11
|
+
byPathName: Map<string, string[]>;
|
|
12
|
+
byName: Map<string, string[]>;
|
|
13
|
+
byPackageName: Map<string, string[]>;
|
|
14
|
+
}
|
|
15
|
+
export interface PublicApiRefIndex {
|
|
16
|
+
byId: Map<string, PublicApiRecord>;
|
|
17
|
+
byPathName: Map<string, string[]>;
|
|
18
|
+
byName: Map<string, string[]>;
|
|
19
|
+
byPackageName: Map<string, string[]>;
|
|
20
|
+
symbolById: Map<string, SymbolRecord>;
|
|
21
|
+
}
|
|
22
|
+
export declare function isGeneratedArtefactPath(filePath: string): boolean;
|
|
23
|
+
/** Prefer source symbols/APIs over generated dist/build artefacts when ranking matches. */
|
|
24
|
+
export declare function preferSourceIds(ids: string[], filePathForId: (id: string) => string | undefined): string[];
|
|
25
|
+
export declare function buildSymbolRefIndex(symbols: SymbolRecord[], packageByFilePath?: Map<string, PackageRecord>): SymbolRefIndex;
|
|
26
|
+
export declare function buildPublicApiRefIndex(publicApis: PublicApiRecord[], symbols: SymbolRecord[], packageByFilePath?: Map<string, PackageRecord>): PublicApiRefIndex;
|
|
27
|
+
export declare function resolveSymbolReference(reference: string, index: SymbolRefIndex): RefResolution;
|
|
28
|
+
export declare function resolvePublicApiReference(reference: string, index: PublicApiRefIndex): RefResolution;
|
|
29
|
+
/**
|
|
30
|
+
* Parse `symbol:path/to/file.ts:Name:12` or `symbol:path/to/file.ts:Name`.
|
|
31
|
+
* Relative catalogue paths are POSIX-style and do not contain colons.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseSymbolPathName(ref: string): {
|
|
34
|
+
filePath: string;
|
|
35
|
+
name: string;
|
|
36
|
+
} | undefined;
|
|
37
|
+
export declare function parsePublicApiPathName(ref: string): {
|
|
38
|
+
filePath: string;
|
|
39
|
+
name: string;
|
|
40
|
+
} | undefined;
|