@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +262 -0
  4. package/dist/catalogue/canonicalisation.d.ts +36 -0
  5. package/dist/catalogue/canonicalisation.js +262 -0
  6. package/dist/catalogue/config.d.ts +13 -0
  7. package/dist/catalogue/config.js +77 -0
  8. package/dist/catalogue/deterministic.d.ts +3 -0
  9. package/dist/catalogue/deterministic.js +25 -0
  10. package/dist/catalogue/diagnostics.d.ts +2 -0
  11. package/dist/catalogue/diagnostics.js +34 -0
  12. package/dist/catalogue/evidence.d.ts +1 -0
  13. package/dist/catalogue/evidence.js +10 -0
  14. package/dist/catalogue/examples.d.ts +3 -0
  15. package/dist/catalogue/examples.js +20 -0
  16. package/dist/catalogue/identity.d.ts +61 -0
  17. package/dist/catalogue/identity.js +114 -0
  18. package/dist/catalogue/ignore.d.ts +4 -0
  19. package/dist/catalogue/ignore.js +29 -0
  20. package/dist/catalogue/indexer.d.ts +20 -0
  21. package/dist/catalogue/indexer.js +1393 -0
  22. package/dist/catalogue/linkage.d.ts +16 -0
  23. package/dist/catalogue/linkage.js +419 -0
  24. package/dist/catalogue/moduleResolution.d.ts +4 -0
  25. package/dist/catalogue/moduleResolution.js +67 -0
  26. package/dist/catalogue/ordering.d.ts +13 -0
  27. package/dist/catalogue/ordering.js +86 -0
  28. package/dist/catalogue/packages.d.ts +12 -0
  29. package/dist/catalogue/packages.js +475 -0
  30. package/dist/catalogue/pythonReExports.d.ts +7 -0
  31. package/dist/catalogue/pythonReExports.js +113 -0
  32. package/dist/catalogue/resolveRefs.d.ts +40 -0
  33. package/dist/catalogue/resolveRefs.js +284 -0
  34. package/dist/catalogue/scanner.d.ts +8 -0
  35. package/dist/catalogue/scanner.js +99 -0
  36. package/dist/catalogue/schema.d.ts +289 -0
  37. package/dist/catalogue/schema.js +1 -0
  38. package/dist/catalogue/store.d.ts +41 -0
  39. package/dist/catalogue/store.js +209 -0
  40. package/dist/catalogue/templates.d.ts +18 -0
  41. package/dist/catalogue/templates.js +158 -0
  42. package/dist/catalogue/version.d.ts +12 -0
  43. package/dist/catalogue/version.js +30 -0
  44. package/dist/cli/commands/adoptionPlan.d.ts +8 -0
  45. package/dist/cli/commands/adoptionPlan.js +24 -0
  46. package/dist/cli/commands/auditDocs.d.ts +8 -0
  47. package/dist/cli/commands/auditDocs.js +24 -0
  48. package/dist/cli/commands/changeImpact.d.ts +10 -0
  49. package/dist/cli/commands/changeImpact.js +36 -0
  50. package/dist/cli/commands/changes.d.ts +12 -0
  51. package/dist/cli/commands/changes.js +31 -0
  52. package/dist/cli/commands/check.d.ts +9 -0
  53. package/dist/cli/commands/check.js +4 -0
  54. package/dist/cli/commands/compatibility.d.ts +10 -0
  55. package/dist/cli/commands/compatibility.js +43 -0
  56. package/dist/cli/commands/diff.d.ts +31 -0
  57. package/dist/cli/commands/diff.js +118 -0
  58. package/dist/cli/commands/dogfood.d.ts +8 -0
  59. package/dist/cli/commands/dogfood.js +24 -0
  60. package/dist/cli/commands/drift.d.ts +8 -0
  61. package/dist/cli/commands/drift.js +13 -0
  62. package/dist/cli/commands/index.d.ts +9 -0
  63. package/dist/cli/commands/index.js +9 -0
  64. package/dist/cli/commands/indexWorkspace.d.ts +8 -0
  65. package/dist/cli/commands/indexWorkspace.js +23 -0
  66. package/dist/cli/commands/recommendConfig.d.ts +7 -0
  67. package/dist/cli/commands/recommendConfig.js +20 -0
  68. package/dist/cli/commands/schema.d.ts +1 -0
  69. package/dist/cli/commands/schema.js +37 -0
  70. package/dist/cli/commands/template.d.ts +5 -0
  71. package/dist/cli/commands/template.js +44 -0
  72. package/dist/cli/commands/validate.d.ts +9 -0
  73. package/dist/cli/commands/validate.js +85 -0
  74. package/dist/cli/index.d.ts +2 -0
  75. package/dist/cli/index.js +211 -0
  76. package/dist/comparison/artefactValidate.d.ts +19 -0
  77. package/dist/comparison/artefactValidate.js +334 -0
  78. package/dist/comparison/candidates.d.ts +12 -0
  79. package/dist/comparison/candidates.js +391 -0
  80. package/dist/comparison/compare.d.ts +15 -0
  81. package/dist/comparison/compare.js +240 -0
  82. package/dist/comparison/compatibility.d.ts +26 -0
  83. package/dist/comparison/compatibility.js +316 -0
  84. package/dist/comparison/drift.d.ts +34 -0
  85. package/dist/comparison/drift.js +71 -0
  86. package/dist/comparison/identity.d.ts +30 -0
  87. package/dist/comparison/identity.js +73 -0
  88. package/dist/comparison/impact.d.ts +14 -0
  89. package/dist/comparison/impact.js +291 -0
  90. package/dist/comparison/index.d.ts +6 -0
  91. package/dist/comparison/index.js +6 -0
  92. package/dist/comparison/integrity.d.ts +12 -0
  93. package/dist/comparison/integrity.js +199 -0
  94. package/dist/comparison/ordering.d.ts +4 -0
  95. package/dist/comparison/ordering.js +69 -0
  96. package/dist/comparison/projections.d.ts +19 -0
  97. package/dist/comparison/projections.js +185 -0
  98. package/dist/comparison/relationshipPolicy.d.ts +19 -0
  99. package/dist/comparison/relationshipPolicy.js +98 -0
  100. package/dist/comparison/schema.d.ts +178 -0
  101. package/dist/comparison/schema.js +16 -0
  102. package/dist/comparison/store.d.ts +58 -0
  103. package/dist/comparison/store.js +462 -0
  104. package/dist/comparison/validate.d.ts +23 -0
  105. package/dist/comparison/validate.js +137 -0
  106. package/dist/comparison/version.d.ts +9 -0
  107. package/dist/comparison/version.js +11 -0
  108. package/dist/extractors/docTags.d.ts +9 -0
  109. package/dist/extractors/docTags.js +106 -0
  110. package/dist/extractors/generic.d.ts +5 -0
  111. package/dist/extractors/generic.js +3 -0
  112. package/dist/extractors/javascript.d.ts +3 -0
  113. package/dist/extractors/javascript.js +4 -0
  114. package/dist/extractors/markdown.d.ts +7 -0
  115. package/dist/extractors/markdown.js +269 -0
  116. package/dist/extractors/python.d.ts +7 -0
  117. package/dist/extractors/python.js +314 -0
  118. package/dist/extractors/typescript.d.ts +7 -0
  119. package/dist/extractors/typescript.js +387 -0
  120. package/dist/index.d.ts +2 -0
  121. package/dist/index.js +2 -0
  122. package/dist/mcp/resources.d.ts +18 -0
  123. package/dist/mcp/resources.js +57 -0
  124. package/dist/mcp/server.d.ts +2 -0
  125. package/dist/mcp/server.js +95 -0
  126. package/dist/mcp/tools.d.ts +29 -0
  127. package/dist/mcp/tools.js +902 -0
  128. package/dist/queries/adoption.d.ts +62 -0
  129. package/dist/queries/adoption.js +366 -0
  130. package/dist/queries/capabilities.d.ts +45 -0
  131. package/dist/queries/capabilities.js +124 -0
  132. package/dist/queries/changes.d.ts +33 -0
  133. package/dist/queries/changes.js +53 -0
  134. package/dist/queries/dependencies.d.ts +17 -0
  135. package/dist/queries/dependencies.js +94 -0
  136. package/dist/queries/dogfood.d.ts +50 -0
  137. package/dist/queries/dogfood.js +116 -0
  138. package/dist/queries/editingContext.d.ts +84 -0
  139. package/dist/queries/editingContext.js +240 -0
  140. package/dist/queries/examples.d.ts +8 -0
  141. package/dist/queries/examples.js +16 -0
  142. package/dist/queries/files.d.ts +8 -0
  143. package/dist/queries/files.js +14 -0
  144. package/dist/queries/lists.d.ts +94 -0
  145. package/dist/queries/lists.js +184 -0
  146. package/dist/queries/overview.d.ts +2 -0
  147. package/dist/queries/overview.js +8 -0
  148. package/dist/queries/publicApis.d.ts +60 -0
  149. package/dist/queries/publicApis.js +120 -0
  150. package/dist/queries/ranking.d.ts +67 -0
  151. package/dist/queries/ranking.js +387 -0
  152. package/dist/queries/recommendConfig.d.ts +47 -0
  153. package/dist/queries/recommendConfig.js +123 -0
  154. package/dist/queries/relationships.d.ts +5 -0
  155. package/dist/queries/relationships.js +15 -0
  156. package/dist/queries/reuseCandidates.d.ts +34 -0
  157. package/dist/queries/reuseCandidates.js +181 -0
  158. package/dist/queries/symbols.d.ts +40 -0
  159. package/dist/queries/symbols.js +41 -0
  160. package/dist/security/pathPolicy.d.ts +12 -0
  161. package/dist/security/pathPolicy.js +46 -0
  162. package/dist/security/secretPatterns.d.ts +1 -0
  163. package/dist/security/secretPatterns.js +4 -0
  164. package/dist/security/sourceAccess.d.ts +12 -0
  165. package/dist/security/sourceAccess.js +35 -0
  166. package/package.json +60 -0
@@ -0,0 +1,16 @@
1
+ import type { FileRecord, PublicApiRecord, RelationshipRecord, SymbolRecord } from "./schema.js";
2
+ import type { PackageModel } from "./packages.js";
3
+ export interface LinkageInput {
4
+ workspaceRoot: string;
5
+ files: FileRecord[];
6
+ symbols: SymbolRecord[];
7
+ publicApis: PublicApiRecord[];
8
+ relationships: RelationshipRecord[];
9
+ packageModel: PackageModel;
10
+ }
11
+ /**
12
+ * Build all Gate 3 linkage relationships. Endpoints are canonical (duplicate
13
+ * declarations collapse onto their preferred source), IDs come from
14
+ * `relationshipIdentityId`, and duplicate edges merge their evidence.
15
+ */
16
+ export declare function buildLinkageRelationships(input: LinkageInput): RelationshipRecord[];
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Gate 3 linkage: provenanced, conservative consumer→API relationships.
3
+ *
4
+ * This module derives two relationship kinds and never anything weaker:
5
+ * - `resolved_direct_import`: a file imports a binding that resolves, through
6
+ * module resolution, to a catalogue public API or symbol. The binding may or
7
+ * may not be used at runtime.
8
+ * - `resolved_direct_usage`: a resolved, runtime (non type-only) imported
9
+ * binding is actually referenced in the same file.
10
+ *
11
+ * Deliberately NOT emitted here (kept as diagnostics / ranking evidence only):
12
+ * - package proximity links
13
+ * - name-reference links
14
+ * Inferred imports/usages are never relabelled as `covers`/`explicit_coverage`.
15
+ */
16
+ import path from "node:path";
17
+ import { readFileSync } from "node:fs";
18
+ import ts from "typescript";
19
+ import { relationshipIdentityId } from "./identity.js";
20
+ import { truncateEvidenceText } from "./evidence.js";
21
+ import { isGeneratedArtefactPath } from "./resolveRefs.js";
22
+ import { resolvePythonRelativeModuleTarget, resolveTypeScriptModuleTarget } from "./moduleResolution.js";
23
+ /**
24
+ * Build all Gate 3 linkage relationships. Endpoints are canonical (duplicate
25
+ * declarations collapse onto their preferred source), IDs come from
26
+ * `relationshipIdentityId`, and duplicate edges merge their evidence.
27
+ */
28
+ export function buildLinkageRelationships(input) {
29
+ const resolver = new TargetResolver(input);
30
+ const merged = new Map();
31
+ const emit = (type, filePath, target, discriminant, confidence, binding, language) => {
32
+ const from = { kind: "file", id: filePath };
33
+ const id = relationshipIdentityId({
34
+ type,
35
+ fromKind: from.kind,
36
+ fromId: from.id,
37
+ toKind: target.kind,
38
+ toId: target.id,
39
+ discriminant
40
+ });
41
+ const evidence = {
42
+ filePath,
43
+ startLine: binding.line,
44
+ endLine: binding.line,
45
+ text: truncateEvidenceText(binding.text)
46
+ };
47
+ const existing = merged.get(id);
48
+ if (existing) {
49
+ existing.evidenceRanges = [...(existing.evidenceRanges ?? []), evidence];
50
+ return;
51
+ }
52
+ merged.set(id, {
53
+ id,
54
+ type,
55
+ from,
56
+ to: target,
57
+ localDiscriminant: discriminant,
58
+ evidence,
59
+ confidence,
60
+ provenance: {
61
+ source: language === "python" ? "python-static" : "typescript-ast",
62
+ extractors: ["linkage"]
63
+ }
64
+ });
65
+ };
66
+ for (const file of input.files) {
67
+ if (file.language !== "typescript" && file.language !== "javascript" && file.language !== "python") {
68
+ continue;
69
+ }
70
+ let bindings;
71
+ try {
72
+ bindings = file.language === "python"
73
+ ? analyzePythonImports(input.workspaceRoot, file)
74
+ : analyzeTypeScriptImports(input.workspaceRoot, file);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ for (const binding of bindings) {
80
+ // Namespace and side-effect imports are intentionally ambiguous: we do not
81
+ // know which member(s) resolve, so no per-symbol edge and no usage edge.
82
+ if (binding.externalName === null) {
83
+ continue;
84
+ }
85
+ const resolved = resolver.resolve(file, binding.moduleSpecifier, binding.externalName, binding.kind);
86
+ if (!resolved) {
87
+ continue;
88
+ }
89
+ const target = resolved.publicApiId
90
+ ? { kind: "publicApi", id: resolved.publicApiId }
91
+ : { kind: "symbol", id: resolved.symbolId };
92
+ emit("resolved_direct_import", file.path, target, binding.kind, 0.9, binding, file.language);
93
+ // Usage requires a runtime (non type-only) reference to the local binding.
94
+ if (!binding.typeOnly && binding.used) {
95
+ emit("resolved_direct_usage", file.path, target, "runtime", 0.85, binding, file.language);
96
+ }
97
+ }
98
+ }
99
+ return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
100
+ }
101
+ /**
102
+ * Resolves an imported `(moduleSpecifier, externalName)` pair to a catalogue
103
+ * symbol by first resolving the module path (so identically named exports in
104
+ * different packages never cross-link) and then matching the exported name,
105
+ * following explicit re-export edges.
106
+ */
107
+ class TargetResolver {
108
+ workspaceRoot;
109
+ symbolById;
110
+ publicApiBySymbolId;
111
+ /** filePath -> exported symbol ids emitted via `exports` edges to symbols. */
112
+ exportSymbolsByFile = new Map();
113
+ /** filePath -> default-exported symbol ids. */
114
+ defaultExportsByFile = new Map();
115
+ /** filePath -> re-export target file paths (`export ... from './x'`). */
116
+ reExportFilesByFile = new Map();
117
+ /** filePath -> top-level canonical symbol ids defined in the file. */
118
+ definedSymbolsByFile = new Map();
119
+ constructor(input) {
120
+ this.workspaceRoot = input.workspaceRoot;
121
+ this.symbolById = new Map(input.symbols.map((symbol) => [symbol.id, symbol]));
122
+ this.publicApiBySymbolId = new Map(input.publicApis.map((api) => [api.symbolId, api.id]));
123
+ for (const symbol of input.symbols) {
124
+ if (symbol.duplicateOf) {
125
+ continue;
126
+ }
127
+ if ((symbol.qualifiedContainerPath ?? "") !== "") {
128
+ continue;
129
+ }
130
+ pushUnique(this.definedSymbolsByFile, symbol.filePath, symbol.id);
131
+ }
132
+ for (const relationship of input.relationships) {
133
+ if (relationship.type !== "exports" || relationship.from.kind !== "file") {
134
+ continue;
135
+ }
136
+ if (relationship.to.kind === "symbol") {
137
+ pushUnique(this.exportSymbolsByFile, relationship.from.id, relationship.to.id);
138
+ if (relationship.localDiscriminant === "default") {
139
+ pushUnique(this.defaultExportsByFile, relationship.from.id, relationship.to.id);
140
+ }
141
+ }
142
+ else if (relationship.to.kind === "file") {
143
+ pushUnique(this.reExportFilesByFile, relationship.from.id, relationship.to.id);
144
+ }
145
+ }
146
+ }
147
+ resolve(file, moduleSpecifier, externalName, kind) {
148
+ const targetFile = file.language === "python"
149
+ ? resolvePythonRelativeModuleTarget(this.workspaceRoot, file.path, moduleSpecifier)
150
+ : resolveTypeScriptModuleTarget(this.workspaceRoot, file.path, moduleSpecifier);
151
+ if (!targetFile) {
152
+ return undefined;
153
+ }
154
+ const symbolId = this.findExportedSymbol(targetFile, externalName, kind, new Set());
155
+ if (!symbolId) {
156
+ return undefined;
157
+ }
158
+ return { symbolId, publicApiId: this.publicApiBySymbolId.get(symbolId) };
159
+ }
160
+ findExportedSymbol(filePath, externalName, kind, visited) {
161
+ if (visited.has(filePath)) {
162
+ return undefined;
163
+ }
164
+ visited.add(filePath);
165
+ if (kind === "default") {
166
+ const defaults = this.defaultExportsByFile.get(filePath) ?? [];
167
+ const preferred = this.preferSource(defaults);
168
+ if (preferred) {
169
+ return preferred;
170
+ }
171
+ }
172
+ const candidates = [];
173
+ for (const symbolId of this.exportSymbolsByFile.get(filePath) ?? []) {
174
+ if (this.symbolById.get(symbolId)?.name === externalName) {
175
+ candidates.push(symbolId);
176
+ }
177
+ }
178
+ // Fall back to a top-level declaration with a matching name (covers cases
179
+ // where the export edge points at a file rather than a specific symbol).
180
+ for (const symbolId of this.definedSymbolsByFile.get(filePath) ?? []) {
181
+ const symbol = this.symbolById.get(symbolId);
182
+ if (symbol?.name === externalName && symbol.exported) {
183
+ candidates.push(symbolId);
184
+ }
185
+ }
186
+ const preferred = this.preferSource(candidates);
187
+ if (preferred) {
188
+ return preferred;
189
+ }
190
+ for (const reExportTarget of this.reExportFilesByFile.get(filePath) ?? []) {
191
+ const found = this.findExportedSymbol(reExportTarget, externalName, kind, visited);
192
+ if (found) {
193
+ return found;
194
+ }
195
+ }
196
+ return undefined;
197
+ }
198
+ preferSource(ids) {
199
+ if (ids.length === 0) {
200
+ return undefined;
201
+ }
202
+ const unique = [...new Set(ids)];
203
+ const authored = unique.filter((id) => {
204
+ const filePath = this.symbolById.get(id)?.filePath;
205
+ return filePath ? !isGeneratedArtefactPath(filePath) : true;
206
+ });
207
+ const pool = authored.length > 0 ? authored : unique;
208
+ // Deterministic selection when several remain (rare): lowest id.
209
+ return [...pool].sort()[0];
210
+ }
211
+ }
212
+ function analyzeTypeScriptImports(workspaceRoot, file) {
213
+ const absolute = path.resolve(workspaceRoot, file.path);
214
+ const sourceText = readFileSync(absolute, "utf8");
215
+ const sourceFile = ts.createSourceFile(file.path, sourceText, ts.ScriptTarget.ES2022, true);
216
+ const bindings = [];
217
+ const localNames = new Set();
218
+ for (const statement of sourceFile.statements) {
219
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
220
+ continue;
221
+ }
222
+ const moduleSpecifier = statement.moduleSpecifier.text;
223
+ const start = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
224
+ const line = start.line + 1;
225
+ const text = statement.getText(sourceFile);
226
+ const clause = statement.importClause;
227
+ if (!clause) {
228
+ bindings.push({ moduleSpecifier, externalName: null, localName: null, kind: "side-effect", typeOnly: false, line, text });
229
+ continue;
230
+ }
231
+ const clauseTypeOnly = clause.isTypeOnly === true;
232
+ if (clause.name) {
233
+ bindings.push({
234
+ moduleSpecifier,
235
+ externalName: "default",
236
+ localName: clause.name.text,
237
+ kind: "default",
238
+ typeOnly: clauseTypeOnly,
239
+ line,
240
+ text
241
+ });
242
+ localNames.add(clause.name.text);
243
+ }
244
+ const named = clause.namedBindings;
245
+ if (named && ts.isNamespaceImport(named)) {
246
+ bindings.push({
247
+ moduleSpecifier,
248
+ externalName: null,
249
+ localName: named.name.text,
250
+ kind: "namespace",
251
+ typeOnly: clauseTypeOnly,
252
+ line,
253
+ text
254
+ });
255
+ }
256
+ else if (named && ts.isNamedImports(named)) {
257
+ for (const element of named.elements) {
258
+ const externalName = (element.propertyName ?? element.name).text;
259
+ const localName = element.name.text;
260
+ const typeOnly = clauseTypeOnly || element.isTypeOnly === true;
261
+ const aliased = Boolean(element.propertyName) && element.propertyName.text !== localName;
262
+ bindings.push({
263
+ moduleSpecifier,
264
+ externalName,
265
+ localName,
266
+ kind: typeOnly ? "type-only" : aliased ? "aliased" : "named",
267
+ typeOnly,
268
+ line,
269
+ text
270
+ });
271
+ localNames.add(localName);
272
+ }
273
+ }
274
+ }
275
+ if (bindings.length === 0) {
276
+ return [];
277
+ }
278
+ const used = collectTypeScriptValueIdentifiers(sourceFile, localNames);
279
+ return bindings.map((binding) => ({
280
+ ...binding,
281
+ used: binding.localName ? used.has(binding.localName) : false
282
+ }));
283
+ }
284
+ /**
285
+ * Collect identifiers that appear in *value* positions outside import
286
+ * declarations. Type-only references (inside a TypeNode) are excluded so that a
287
+ * value import used only in a type annotation is not counted as runtime usage.
288
+ */
289
+ function collectTypeScriptValueIdentifiers(sourceFile, interesting) {
290
+ const used = new Set();
291
+ const walk = (node) => {
292
+ if (ts.isImportDeclaration(node)) {
293
+ return;
294
+ }
295
+ if (ts.isTypeNode(node) || ts.isTypeReferenceNode(node)) {
296
+ return;
297
+ }
298
+ if (ts.isIdentifier(node) && interesting.has(node.text)) {
299
+ used.add(node.text);
300
+ }
301
+ ts.forEachChild(node, walk);
302
+ };
303
+ walk(sourceFile);
304
+ return used;
305
+ }
306
+ function analyzePythonImports(workspaceRoot, file) {
307
+ const absolute = path.resolve(workspaceRoot, file.path);
308
+ const sourceText = readFileSync(absolute, "utf8");
309
+ const lines = sourceText.split(/\r?\n/);
310
+ const typeCheckingRanges = collectTypeCheckingRanges(lines);
311
+ const bindings = [];
312
+ const localNames = new Set();
313
+ const importLineNumbers = new Set();
314
+ for (let i = 0; i < lines.length; i += 1) {
315
+ const line = lines[i] ?? "";
316
+ const fromImport = line.match(/^\s*from\s+([.A-Za-z_][A-Za-z0-9_.]*)\s+import\s+(.+)$/);
317
+ if (!fromImport) {
318
+ continue;
319
+ }
320
+ const moduleSpecifier = fromImport[1]?.trim() ?? "";
321
+ // Only relative (intra-repo) imports can resolve to catalogue files.
322
+ if (!moduleSpecifier.startsWith(".")) {
323
+ continue;
324
+ }
325
+ const typeOnly = typeCheckingRanges.some((range) => i >= range.start && i <= range.end);
326
+ const text = line.trim();
327
+ importLineNumbers.add(i);
328
+ const importList = stripImportWrappers(fromImport[2] ?? "");
329
+ for (const rawEntry of importList.split(",")) {
330
+ const entry = rawEntry.trim();
331
+ if (!entry || entry === "*") {
332
+ continue;
333
+ }
334
+ const parts = entry.split(/\s+as\s+/i);
335
+ const externalName = parts[0]?.trim();
336
+ const localName = (parts[1] ?? parts[0])?.trim();
337
+ if (!externalName || !localName || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(externalName)) {
338
+ continue;
339
+ }
340
+ const aliased = Boolean(parts[1]) && parts[1].trim() !== externalName;
341
+ bindings.push({
342
+ moduleSpecifier,
343
+ externalName,
344
+ localName,
345
+ kind: typeOnly ? "type-only" : aliased ? "aliased" : "named",
346
+ typeOnly,
347
+ line: i + 1,
348
+ text
349
+ });
350
+ localNames.add(localName);
351
+ }
352
+ }
353
+ if (bindings.length === 0) {
354
+ return [];
355
+ }
356
+ const used = collectPythonValueIdentifiers(lines, localNames, importLineNumbers, typeCheckingRanges);
357
+ return bindings.map((binding) => ({
358
+ ...binding,
359
+ used: binding.localName ? used.has(binding.localName) : false
360
+ }));
361
+ }
362
+ function stripImportWrappers(value) {
363
+ return value.replace(/[()\\]/g, " ").trim();
364
+ }
365
+ /** Detect `if TYPE_CHECKING:` blocks and return their indented line ranges. */
366
+ function collectTypeCheckingRanges(lines) {
367
+ const ranges = [];
368
+ for (let i = 0; i < lines.length; i += 1) {
369
+ const line = lines[i] ?? "";
370
+ const match = line.match(/^(\s*)if\s+(?:typing\.)?TYPE_CHECKING\s*:/);
371
+ if (!match) {
372
+ continue;
373
+ }
374
+ const headerIndent = match[1]?.length ?? 0;
375
+ let end = i;
376
+ for (let j = i + 1; j < lines.length; j += 1) {
377
+ const body = lines[j] ?? "";
378
+ if (body.trim() === "") {
379
+ end = j;
380
+ continue;
381
+ }
382
+ const indent = body.match(/^\s*/)?.[0].length ?? 0;
383
+ if (indent <= headerIndent) {
384
+ break;
385
+ }
386
+ end = j;
387
+ }
388
+ ranges.push({ start: i + 1, end });
389
+ }
390
+ return ranges;
391
+ }
392
+ function collectPythonValueIdentifiers(lines, interesting, importLineNumbers, typeCheckingRanges) {
393
+ const used = new Set();
394
+ for (let i = 0; i < lines.length; i += 1) {
395
+ if (importLineNumbers.has(i)) {
396
+ continue;
397
+ }
398
+ if (typeCheckingRanges.some((range) => i >= range.start && i <= range.end)) {
399
+ continue;
400
+ }
401
+ let line = lines[i] ?? "";
402
+ // Drop comments and string literals to avoid counting textual mentions.
403
+ line = line.replace(/#.*$/, "").replace(/(["']).*?\1/g, " ");
404
+ for (const token of line.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
405
+ const name = token[0];
406
+ if (interesting.has(name)) {
407
+ used.add(name);
408
+ }
409
+ }
410
+ }
411
+ return used;
412
+ }
413
+ function pushUnique(map, key, value) {
414
+ const current = map.get(key) ?? [];
415
+ if (!current.includes(value)) {
416
+ current.push(value);
417
+ map.set(key, current);
418
+ }
419
+ }
@@ -0,0 +1,4 @@
1
+ export declare function toPosixPath(filePath: string): string;
2
+ export declare function createExternalPackageId(specifier: string): string;
3
+ export declare function resolveTypeScriptModuleTarget(workspaceRoot: string, fromFilePath: string, specifier: string): string | undefined;
4
+ export declare function resolvePythonRelativeModuleTarget(workspaceRoot: string, fromFilePath: string, modulePath: string): string | undefined;
@@ -0,0 +1,67 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ const TS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
4
+ const TS_INDEX_EXTENSIONS = ["index.ts", "index.tsx", "index.js", "index.jsx", "index.mjs", "index.cjs"];
5
+ export function toPosixPath(filePath) {
6
+ return filePath.replaceAll("\\", "/");
7
+ }
8
+ export function createExternalPackageId(specifier) {
9
+ return `external:${specifier}`;
10
+ }
11
+ export function resolveTypeScriptModuleTarget(workspaceRoot, fromFilePath, specifier) {
12
+ if (!specifier.startsWith(".")) {
13
+ return undefined;
14
+ }
15
+ const fromDir = path.resolve(workspaceRoot, path.dirname(fromFilePath));
16
+ const absoluteBase = path.resolve(fromDir, specifier);
17
+ const candidates = [absoluteBase];
18
+ const extension = path.extname(absoluteBase).toLowerCase();
19
+ if ([".js", ".mjs", ".cjs"].includes(extension)) {
20
+ candidates.push(absoluteBase.slice(0, -extension.length));
21
+ }
22
+ if (extension === ".jsx") {
23
+ candidates.push(absoluteBase.slice(0, -extension.length));
24
+ }
25
+ return resolveByProbing(workspaceRoot, candidates, TS_EXTENSIONS, TS_INDEX_EXTENSIONS);
26
+ }
27
+ export function resolvePythonRelativeModuleTarget(workspaceRoot, fromFilePath, modulePath) {
28
+ if (!modulePath.startsWith(".")) {
29
+ return undefined;
30
+ }
31
+ const leadingDots = modulePath.match(/^\.+/)?.[0].length ?? 0;
32
+ if (leadingDots === 0) {
33
+ return undefined;
34
+ }
35
+ const relativeName = modulePath.slice(leadingDots).replaceAll(".", path.sep);
36
+ let baseDir = path.resolve(workspaceRoot, path.dirname(fromFilePath));
37
+ for (let step = 1; step < leadingDots; step += 1) {
38
+ baseDir = path.dirname(baseDir);
39
+ }
40
+ const absoluteBase = relativeName ? path.resolve(baseDir, relativeName) : baseDir;
41
+ const probeOrder = [".py"];
42
+ const indexOrder = ["__init__.py"];
43
+ return resolveByProbing(workspaceRoot, [absoluteBase], probeOrder, indexOrder);
44
+ }
45
+ function resolveByProbing(workspaceRoot, absoluteBases, extensions, indexFiles) {
46
+ const candidates = [];
47
+ for (const absoluteBase of absoluteBases) {
48
+ candidates.push(absoluteBase);
49
+ for (const extension of extensions) {
50
+ candidates.push(`${absoluteBase}${extension}`);
51
+ }
52
+ for (const indexFile of indexFiles) {
53
+ candidates.push(path.join(absoluteBase, indexFile));
54
+ }
55
+ }
56
+ for (const candidate of candidates) {
57
+ const normalized = path.normalize(candidate);
58
+ if (!normalized.startsWith(path.normalize(workspaceRoot))) {
59
+ continue;
60
+ }
61
+ if (existsSync(normalized)) {
62
+ const relative = path.relative(workspaceRoot, normalized);
63
+ return toPosixPath(relative);
64
+ }
65
+ }
66
+ return undefined;
67
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Deterministic semantic ordering for resources and list tools.
3
+ */
4
+ import type { CapabilityRecord, DiagnosticRecord, ExampleRecord, FileRecord, PackageRecord, PublicApiRecord, RelationshipRecord, SymbolRecord, TestRecord } from "./schema.js";
5
+ export declare function sortPackages(records: PackageRecord[]): PackageRecord[];
6
+ export declare function sortFiles(records: FileRecord[]): FileRecord[];
7
+ export declare function sortSymbols(records: SymbolRecord[]): SymbolRecord[];
8
+ export declare function sortPublicApis(records: PublicApiRecord[]): PublicApiRecord[];
9
+ export declare function sortCapabilities(records: CapabilityRecord[]): CapabilityRecord[];
10
+ export declare function sortExamples(records: ExampleRecord[]): ExampleRecord[];
11
+ export declare function sortRelationships(records: RelationshipRecord[]): RelationshipRecord[];
12
+ export declare function sortTests(records: TestRecord[]): TestRecord[];
13
+ export declare function sortDiagnostics(records: DiagnosticRecord[]): DiagnosticRecord[];
@@ -0,0 +1,86 @@
1
+ import { normalizeIdentityString } from "./identity.js";
2
+ function cmp(a, b) {
3
+ const left = normalizeIdentityString(a);
4
+ const right = normalizeIdentityString(b);
5
+ if (left < right)
6
+ return -1;
7
+ if (left > right)
8
+ return 1;
9
+ return 0;
10
+ }
11
+ function cmpNullable(a, b) {
12
+ return cmp(a ?? "", b ?? "");
13
+ }
14
+ export function sortPackages(records) {
15
+ return [...records].sort((a, b) => {
16
+ return (cmp(a.ecosystem ?? "unknown", b.ecosystem ?? "unknown") ||
17
+ cmp(a.name, b.name) ||
18
+ cmp(a.path, b.path) ||
19
+ cmp(a.id, b.id));
20
+ });
21
+ }
22
+ export function sortFiles(records) {
23
+ return [...records].sort((a, b) => {
24
+ return cmpNullable(a.packageName, b.packageName) || cmp(a.path, b.path);
25
+ });
26
+ }
27
+ export function sortSymbols(records) {
28
+ return [...records].sort((a, b) => {
29
+ return (cmpNullable(a.packageId, b.packageId) ||
30
+ cmp(a.filePath, b.filePath) ||
31
+ cmp(a.qualifiedName, b.qualifiedName) ||
32
+ cmp(a.kind, b.kind) ||
33
+ cmp(a.id, b.id));
34
+ });
35
+ }
36
+ export function sortPublicApis(records) {
37
+ return [...records].sort((a, b) => {
38
+ return (cmpNullable(a.packageId, b.packageId) ||
39
+ cmpNullable(a.importPath, b.importPath) ||
40
+ cmp(a.name, b.name) ||
41
+ cmp(a.exportKind ?? "named", b.exportKind ?? "named") ||
42
+ cmp(a.id, b.id));
43
+ });
44
+ }
45
+ export function sortCapabilities(records) {
46
+ return [...records].sort((a, b) => {
47
+ const aPkg = a.packages[0] ?? "";
48
+ const bPkg = b.packages[0] ?? "";
49
+ return cmp(aPkg, bPkg) || cmp(a.name, b.name) || cmp(a.id, b.id);
50
+ });
51
+ }
52
+ export function sortExamples(records) {
53
+ return [...records].sort((a, b) => {
54
+ return cmpNullable(a.filePath, b.filePath) || cmpNullable(a.title, b.title) || cmp(a.id, b.id);
55
+ });
56
+ }
57
+ export function sortRelationships(records) {
58
+ return [...records].sort((a, b) => {
59
+ return (cmp(a.type, b.type) ||
60
+ cmp(a.from.id, b.from.id) ||
61
+ cmp(a.to.id, b.to.id) ||
62
+ cmp(a.localDiscriminant ?? "", b.localDiscriminant ?? "") ||
63
+ cmp(a.id, b.id));
64
+ });
65
+ }
66
+ export function sortTests(records) {
67
+ return [...records].sort((a, b) => {
68
+ return (cmp(a.filePath, b.filePath) ||
69
+ cmpNullable(a.framework, b.framework) ||
70
+ cmpNullable(a.suitePath, b.suitePath) ||
71
+ cmp(a.name, b.name) ||
72
+ cmp(a.id, b.id));
73
+ });
74
+ }
75
+ const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
76
+ export function sortDiagnostics(records) {
77
+ return [...records].sort((a, b) => {
78
+ const sev = (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9);
79
+ if (sev !== 0)
80
+ return sev;
81
+ return (cmp(a.category, b.category) ||
82
+ cmpNullable(a.filePath, b.filePath) ||
83
+ cmp(a.code, b.code) ||
84
+ cmp(a.id, b.id));
85
+ });
86
+ }
@@ -0,0 +1,12 @@
1
+ import type { FileRecord, PackageRecord, RelationshipRecord } from "./schema.js";
2
+ export interface PackageModel {
3
+ packages: PackageRecord[];
4
+ packageByPath: Map<string, PackageRecord>;
5
+ packageById: Map<string, PackageRecord>;
6
+ filePackageByPath: Map<string, PackageRecord>;
7
+ entrypointImportPathByFile: Map<string, string>;
8
+ }
9
+ export declare function buildPackageModel(workspaceRoot: string, files: FileRecord[], markdownPackages: PackageRecord[]): PackageModel;
10
+ export declare function findPackageForFile(model: PackageModel, filePath: string): PackageRecord | undefined;
11
+ export declare function computePublicApiImportPath(workspaceRoot: string, filePath: string, packageRecord: PackageRecord | undefined, language: string, publicImportPath?: string): string;
12
+ export declare function buildPublicImportPathBySymbolId(packageModel: PackageModel, relationships: RelationshipRecord[]): Map<string, string>;