@untangle-code/file-graph 0.0.0-stage → 0.1.1

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 (130) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +284 -2
  4. package/THIRD_PARTY_LICENSES.md +29 -0
  5. package/dist/assignment.d.ts +15 -0
  6. package/dist/assignment.d.ts.map +1 -0
  7. package/dist/assignment.js +75 -0
  8. package/dist/assignment.js.map +1 -0
  9. package/dist/boundaries/endpoints.d.ts +11 -0
  10. package/dist/boundaries/endpoints.d.ts.map +1 -0
  11. package/dist/boundaries/endpoints.js +195 -0
  12. package/dist/boundaries/endpoints.js.map +1 -0
  13. package/dist/boundaries/index.d.ts +9 -0
  14. package/dist/boundaries/index.d.ts.map +1 -0
  15. package/dist/boundaries/index.js +93 -0
  16. package/dist/boundaries/index.js.map +1 -0
  17. package/dist/boundaries/requests.d.ts +12 -0
  18. package/dist/boundaries/requests.d.ts.map +1 -0
  19. package/dist/boundaries/requests.js +311 -0
  20. package/dist/boundaries/requests.js.map +1 -0
  21. package/dist/edges.d.ts +49 -0
  22. package/dist/edges.d.ts.map +1 -0
  23. package/dist/edges.js +223 -0
  24. package/dist/edges.js.map +1 -0
  25. package/dist/file-index.d.ts +37 -0
  26. package/dist/file-index.d.ts.map +1 -0
  27. package/dist/file-index.js +103 -0
  28. package/dist/file-index.js.map +1 -0
  29. package/dist/graph.d.ts +160 -0
  30. package/dist/graph.d.ts.map +1 -0
  31. package/dist/graph.js +448 -0
  32. package/dist/graph.js.map +1 -0
  33. package/dist/index.d.ts +35 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +48 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/metrics.d.ts +67 -0
  38. package/dist/metrics.d.ts.map +1 -0
  39. package/dist/metrics.js +272 -0
  40. package/dist/metrics.js.map +1 -0
  41. package/dist/package-catalog.d.ts +43 -0
  42. package/dist/package-catalog.d.ts.map +1 -0
  43. package/dist/package-catalog.js +136 -0
  44. package/dist/package-catalog.js.map +1 -0
  45. package/dist/paths.d.ts +56 -0
  46. package/dist/paths.d.ts.map +1 -0
  47. package/dist/paths.js +129 -0
  48. package/dist/paths.js.map +1 -0
  49. package/dist/persistence.d.ts +83 -0
  50. package/dist/persistence.d.ts.map +1 -0
  51. package/dist/persistence.js +538 -0
  52. package/dist/persistence.js.map +1 -0
  53. package/dist/resolve/candidates.d.ts +52 -0
  54. package/dist/resolve/candidates.d.ts.map +1 -0
  55. package/dist/resolve/candidates.js +202 -0
  56. package/dist/resolve/candidates.js.map +1 -0
  57. package/dist/resolve/package-exports.d.ts +31 -0
  58. package/dist/resolve/package-exports.d.ts.map +1 -0
  59. package/dist/resolve/package-exports.js +229 -0
  60. package/dist/resolve/package-exports.js.map +1 -0
  61. package/dist/resolve/pipeline.d.ts +11 -0
  62. package/dist/resolve/pipeline.d.ts.map +1 -0
  63. package/dist/resolve/pipeline.js +109 -0
  64. package/dist/resolve/pipeline.js.map +1 -0
  65. package/dist/resolve/resolvers.d.ts +25 -0
  66. package/dist/resolve/resolvers.d.ts.map +1 -0
  67. package/dist/resolve/resolvers.js +367 -0
  68. package/dist/resolve/resolvers.js.map +1 -0
  69. package/dist/restore.d.ts +75 -0
  70. package/dist/restore.d.ts.map +1 -0
  71. package/dist/restore.js +136 -0
  72. package/dist/restore.js.map +1 -0
  73. package/dist/topology/compose.d.ts +27 -0
  74. package/dist/topology/compose.d.ts.map +1 -0
  75. package/dist/topology/compose.js +208 -0
  76. package/dist/topology/compose.js.map +1 -0
  77. package/dist/topology/jsonc.d.ts +38 -0
  78. package/dist/topology/jsonc.d.ts.map +1 -0
  79. package/dist/topology/jsonc.js +137 -0
  80. package/dist/topology/jsonc.js.map +1 -0
  81. package/dist/topology/load.d.ts +61 -0
  82. package/dist/topology/load.d.ts.map +1 -0
  83. package/dist/topology/load.js +436 -0
  84. package/dist/topology/load.js.map +1 -0
  85. package/dist/topology/manifest.d.ts +48 -0
  86. package/dist/topology/manifest.d.ts.map +1 -0
  87. package/dist/topology/manifest.js +109 -0
  88. package/dist/topology/manifest.js.map +1 -0
  89. package/dist/topology/project-config.d.ts +39 -0
  90. package/dist/topology/project-config.d.ts.map +1 -0
  91. package/dist/topology/project-config.js +150 -0
  92. package/dist/topology/project-config.js.map +1 -0
  93. package/dist/topology/workspace-definition.d.ts +26 -0
  94. package/dist/topology/workspace-definition.d.ts.map +1 -0
  95. package/dist/topology/workspace-definition.js +104 -0
  96. package/dist/topology/workspace-definition.js.map +1 -0
  97. package/dist/traverse.d.ts +49 -0
  98. package/dist/traverse.d.ts.map +1 -0
  99. package/dist/traverse.js +167 -0
  100. package/dist/traverse.js.map +1 -0
  101. package/dist/types.d.ts +332 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +2 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +63 -3
  106. package/src/assignment.ts +83 -0
  107. package/src/boundaries/endpoints.ts +273 -0
  108. package/src/boundaries/index.ts +118 -0
  109. package/src/boundaries/requests.ts +411 -0
  110. package/src/edges.ts +334 -0
  111. package/src/file-index.ts +127 -0
  112. package/src/graph.ts +591 -0
  113. package/src/index.ts +58 -0
  114. package/src/metrics.ts +350 -0
  115. package/src/package-catalog.ts +145 -0
  116. package/src/paths.ts +138 -0
  117. package/src/persistence.ts +746 -0
  118. package/src/resolve/candidates.ts +230 -0
  119. package/src/resolve/package-exports.ts +268 -0
  120. package/src/resolve/pipeline.ts +140 -0
  121. package/src/resolve/resolvers.ts +468 -0
  122. package/src/restore.ts +201 -0
  123. package/src/topology/compose.ts +268 -0
  124. package/src/topology/jsonc.ts +147 -0
  125. package/src/topology/load.ts +644 -0
  126. package/src/topology/manifest.ts +132 -0
  127. package/src/topology/project-config.ts +183 -0
  128. package/src/topology/workspace-definition.ts +115 -0
  129. package/src/traverse.ts +232 -0
  130. package/src/types.ts +372 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Die einzelnen Schritte der Resolver-Pipeline (architecture.md §16.2).
3
+ *
4
+ * Jeder Resolver ist rein: gleiche Eingabe, gleiche Ausgabe, kein
5
+ * Dateisystem, keine Uhr. Er liefert entweder ein Ergebnis (Treffer,
6
+ * externes Ziel oder benannter Fehlschlag) oder `null` — und `null` heisst
7
+ * genau „nicht meine Zustaendigkeit", nie „nicht gefunden". Diese
8
+ * Unterscheidung ist der Grund, warum ein nicht aufloesbarer Import am Ende
9
+ * einen SPRECHENDEN Grund traegt statt eines pauschalen „unresolved" (E4).
10
+ */
11
+ import { compareCodepoints } from "@untangle-code/domain";
12
+
13
+ import { dirnameOf, isRelativeSpecifier, joinRelative, normalizePath } from "../paths.js";
14
+ import type { ImportResolverV1, ResolveContextV1, ResolveResultV1 } from "../types.js";
15
+
16
+ import { probeCandidates } from "./candidates.js";
17
+ import type { SubpathResolution } from "./package-exports.js";
18
+ import {
19
+ resolvePackageImports,
20
+ resolvePackageSubpath,
21
+ splitPackageSpecifier,
22
+ } from "./package-exports.js";
23
+
24
+ /**
25
+ * Modulnamen, die die Laufzeit selbst mitbringt. Bewusst als Liste und nicht
26
+ * ueber `node:module`s `builtinModules`: die Liste haengt sonst von der
27
+ * Node-Version der ANALYSIERENDEN Maschine ab, und dieselbe Codebasis haette
28
+ * je nach Rechner eine andere Import-Resolution-Rate.
29
+ */
30
+ export const RUNTIME_BUILTIN_MODULES = new Set([
31
+ "assert",
32
+ "async_hooks",
33
+ "buffer",
34
+ "child_process",
35
+ "cluster",
36
+ "console",
37
+ "constants",
38
+ "crypto",
39
+ "dgram",
40
+ "diagnostics_channel",
41
+ "dns",
42
+ "domain",
43
+ "events",
44
+ "fs",
45
+ "http",
46
+ "http2",
47
+ "https",
48
+ "inspector",
49
+ "module",
50
+ "net",
51
+ "os",
52
+ "path",
53
+ "perf_hooks",
54
+ "process",
55
+ "punycode",
56
+ "querystring",
57
+ "readline",
58
+ "repl",
59
+ "stream",
60
+ "string_decoder",
61
+ "sys",
62
+ "timers",
63
+ "tls",
64
+ "trace_events",
65
+ "tty",
66
+ "url",
67
+ "util",
68
+ "v8",
69
+ "vm",
70
+ "wasi",
71
+ "worker_threads",
72
+ "zlib",
73
+ ]);
74
+
75
+ /** Baut ein `unresolved`-Ergebnis mit Pflichtdetail und geprueften Kandidaten. */
76
+ function unresolved(
77
+ reason: Extract<ResolveResultV1, { status: "unresolved" }>["reason"],
78
+ detail: string,
79
+ tried: readonly string[] = [],
80
+ ): ResolveResultV1 {
81
+ return {
82
+ status: "unresolved",
83
+ reason,
84
+ detail,
85
+ ...(tried.length > 0 ? { rejectedCandidates: [...tried].sort(compareCodepoints) } : {}),
86
+ };
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // 1. Relative Pfade
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export const relativePathResolver: ImportResolverV1 = {
94
+ name: "relative-path",
95
+ supports(specifier) {
96
+ return isRelativeSpecifier(specifier);
97
+ },
98
+ resolve(specifier, context) {
99
+ const base = joinRelative(dirnameOf(context.fromPath), specifier);
100
+ if (base === null) {
101
+ return unresolved(
102
+ "target_file_not_found",
103
+ `relative specifier "${specifier}" from "${context.fromPath}" points above the repository root`,
104
+ );
105
+ }
106
+ const probe = probeCandidates(context, context.fromRepoId, base);
107
+ if (probe.path === null) {
108
+ return unresolved(
109
+ "target_file_not_found",
110
+ `no file matched relative specifier "${specifier}" from "${context.fromPath}"`,
111
+ probe.tried,
112
+ );
113
+ }
114
+ return {
115
+ status: "resolved",
116
+ mechanism: "relative-path",
117
+ targetRepoId: context.fromRepoId,
118
+ targetPath: probe.path,
119
+ };
120
+ },
121
+ };
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // 2. Sprachaliase: Laufzeitmodule und `#`-Subpath-Imports
125
+ // ---------------------------------------------------------------------------
126
+
127
+ export const runtimeBuiltinResolver: ImportResolverV1 = {
128
+ name: "runtime-builtin",
129
+ supports(specifier) {
130
+ return (
131
+ specifier.startsWith("node:") || RUNTIME_BUILTIN_MODULES.has(specifier.split("/")[0] ?? "")
132
+ );
133
+ },
134
+ resolve(specifier) {
135
+ return {
136
+ status: "external",
137
+ mechanism: "language-alias",
138
+ packageName: specifier.startsWith("node:") ? specifier : `node:${specifier}`,
139
+ };
140
+ },
141
+ };
142
+
143
+ export const subpathImportResolver: ImportResolverV1 = {
144
+ name: "subpath-import",
145
+ supports(specifier) {
146
+ return specifier.startsWith("#");
147
+ },
148
+ resolve(specifier, context) {
149
+ const owner = context.fromPackage;
150
+ if (owner === undefined) {
151
+ return unresolved(
152
+ "package_not_in_workspace_catalog",
153
+ `subpath import "${specifier}" requires an owning package, but "${context.fromPath}" is not assigned to one`,
154
+ );
155
+ }
156
+ const matched = resolvePackageImports(owner, specifier);
157
+ return targetOrFailure(matched, context, owner.repoId, specifier, "language-alias", owner.name);
158
+ },
159
+ };
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // 3./4. Projekt-Aliase und konfigurierte Aliase
163
+ // ---------------------------------------------------------------------------
164
+
165
+ function aliasResolver(
166
+ name: string,
167
+ mechanism: "project-alias" | "configured-alias",
168
+ pick: (
169
+ context: ResolveContextV1,
170
+ ) => readonly { pattern: string; targets: string[]; repoId: string; source: string }[],
171
+ ): ImportResolverV1 {
172
+ return {
173
+ name,
174
+ supports(specifier, context) {
175
+ return pick(context).some(
176
+ (rule) =>
177
+ rule.repoId === context.fromRepoId && matchAlias(rule.pattern, specifier) !== null,
178
+ );
179
+ },
180
+ resolve(specifier, context) {
181
+ const tried: string[] = [];
182
+ for (const rule of pick(context)) {
183
+ if (rule.repoId !== context.fromRepoId) continue;
184
+ const captured = matchAlias(rule.pattern, specifier);
185
+ if (captured === null) continue;
186
+ for (const target of rule.targets) {
187
+ const base = target.includes("*") ? target.split("*").join(captured) : target;
188
+ const probe = probeCandidates(context, context.fromRepoId, base);
189
+ tried.push(...probe.tried);
190
+ if (probe.path !== null) {
191
+ return {
192
+ status: "resolved",
193
+ mechanism,
194
+ targetRepoId: context.fromRepoId,
195
+ targetPath: probe.path,
196
+ };
197
+ }
198
+ }
199
+ }
200
+ return unresolved(
201
+ "alias_configured_but_target_missing",
202
+ `an alias rule matched "${specifier}" but none of its targets exists`,
203
+ tried,
204
+ );
205
+ },
206
+ };
207
+ }
208
+
209
+ export const projectAliasResolver = aliasResolver(
210
+ "project-alias",
211
+ "project-alias",
212
+ (context) => context.projectAliases,
213
+ );
214
+
215
+ export const configuredAliasResolver = aliasResolver(
216
+ "configured-alias",
217
+ "configured-alias",
218
+ (context) => context.configuredAliases,
219
+ );
220
+
221
+ /** `"@app/*"` gegen `"@app/util"` → `"util"`; ohne `*` exakter Vergleich. */
222
+ export function matchAlias(pattern: string, specifier: string): string | null {
223
+ const starIndex = pattern.indexOf("*");
224
+ if (starIndex === -1) {
225
+ return pattern === specifier ? "" : null;
226
+ }
227
+ const prefix = pattern.slice(0, starIndex);
228
+ const suffix = pattern.slice(starIndex + 1);
229
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
230
+ return null;
231
+ }
232
+ if (specifier.length < prefix.length + suffix.length) {
233
+ return null;
234
+ }
235
+ return specifier.slice(prefix.length, specifier.length - suffix.length);
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // 5. Workspace-Paketkatalog (architecture.md §16.3)
240
+ // ---------------------------------------------------------------------------
241
+
242
+ export const workspacePackageResolver: ImportResolverV1 = {
243
+ name: "workspace-package",
244
+ supports(specifier, context) {
245
+ const split = splitPackageSpecifier(specifier);
246
+ return split !== null && context.packageCatalog.has(split.name);
247
+ },
248
+ resolve(specifier, context) {
249
+ const split = splitPackageSpecifier(specifier);
250
+ if (split === null) {
251
+ return unresolved(
252
+ "no_resolver_supports_specifier",
253
+ `"${specifier}" is not a usable package specifier`,
254
+ );
255
+ }
256
+ // Aus der Sicht des IMPORTIERENDEN Repositories nachschlagen: derselbe
257
+ // Paketname kann in mehreren Repositories eines Workspace liegen, und
258
+ // gemeint ist dann zuerst das eigene.
259
+ const lookup = context.packageCatalog.lookup(context.fromRepoId, split.name);
260
+ if (lookup.status === "absent") {
261
+ return unresolved(
262
+ "package_not_in_workspace_catalog",
263
+ `package "${split.name}" is not part of the workspace catalog`,
264
+ );
265
+ }
266
+ if (lookup.status === "ambiguous") {
267
+ // NICHT raten. Weder zwei fremde Repositories mit demselben Paketnamen
268
+ // noch zwei gleichnamige Manifeste im EIGENEN Repository sind ohne
269
+ // weitere Angabe entscheidbar; eine willkuerlich gewaehlte Kante saehe
270
+ // wie ein Erfolg aus und zeigte auf das falsche Paket.
271
+ return unresolved(
272
+ "ambiguous_candidates",
273
+ `package "${split.name}" is declared by ${String(lookup.candidates.length)} manifests [${lookup.candidates
274
+ .map((entry) => `${entry.repoId}:${entry.manifestPath}`)
275
+ .join(
276
+ ", ",
277
+ )}] and none of them is the single candidate for "${context.fromPath}" in repository "${context.fromRepoId}"; the reference cannot be attributed`,
278
+ lookup.candidates.map((entry) => entry.manifestPath).sort(compareCodepoints),
279
+ );
280
+ }
281
+ const target = lookup.entry;
282
+ const matched = resolvePackageSubpath(target, split.subpath);
283
+ const mechanism =
284
+ matched.status === "target" && matched.via === "exports"
285
+ ? "package-exports"
286
+ : "workspace-package";
287
+ return targetOrFailure(matched, context, target.repoId, specifier, mechanism, target.name);
288
+ },
289
+ };
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // 6. Installierte Pakete (ausserhalb des Workspace)
293
+ // ---------------------------------------------------------------------------
294
+
295
+ export const installedPackageResolver: ImportResolverV1 = {
296
+ name: "installed-package",
297
+ supports(specifier, context) {
298
+ const split = splitPackageSpecifier(specifier);
299
+ if (split === null) return false;
300
+ return declaredRange(context, split.name) !== null;
301
+ },
302
+ resolve(specifier, context) {
303
+ const split = splitPackageSpecifier(specifier);
304
+ if (split === null) {
305
+ return unresolved(
306
+ "no_resolver_supports_specifier",
307
+ `"${specifier}" is not a usable package specifier`,
308
+ );
309
+ }
310
+ const range = declaredRange(context, split.name);
311
+ if (range === null) {
312
+ return unresolved(
313
+ "package_not_installed",
314
+ `package "${split.name}" is declared by no manifest reachable from "${context.fromPath}"`,
315
+ );
316
+ }
317
+ return {
318
+ status: "external",
319
+ mechanism: "installed-package",
320
+ packageName: split.name,
321
+ declaredVersion: range,
322
+ };
323
+ },
324
+ };
325
+
326
+ function declaredRange(context: ResolveContextV1, packageName: string): string | null {
327
+ const owner = context.fromPackage;
328
+ if (owner !== undefined) {
329
+ const declared = owner.externalDependencies[packageName];
330
+ if (declared !== undefined) return declared;
331
+ }
332
+ // Fallback auf die Wurzel des Repositories: in einem Monorepo stehen
333
+ // Werkzeuge und geteilte Bibliotheken oft nur dort.
334
+ for (const entry of context.packageCatalog.all()) {
335
+ if (entry.repoId !== context.fromRepoId || entry.packageDir !== "") continue;
336
+ const declared = entry.externalDependencies[packageName];
337
+ if (declared !== undefined) return declared;
338
+ }
339
+ return null;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // 7. Python-Module
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /**
347
+ * Python schreibt Importe punktiert (`from ..pkg.mod import x` → `"..pkg.mod"`).
348
+ * Fuehrende Punkte sind relative Schritte nach oben, der Rest ist ein Pfad.
349
+ * Ohne diesen Resolver waere in einem Python-Teilprojekt JEDER Import
350
+ * unaufgeloest — mit ihm ist wenigstens der relative Fall exakt.
351
+ */
352
+ export const pythonModuleResolver: ImportResolverV1 = {
353
+ name: "python-module",
354
+ supports(specifier, context) {
355
+ if (context.fromLanguageId !== "python") return false;
356
+ return specifier !== "" && !specifier.includes("/");
357
+ },
358
+ resolve(specifier, context) {
359
+ const leadingDots = /^\.+/.exec(specifier)?.[0].length ?? 0;
360
+ const rest = specifier.slice(leadingDots);
361
+ const segments = rest === "" ? [] : rest.split(".");
362
+
363
+ if (leadingDots > 0) {
364
+ // Ein Punkt = das eigene Paketverzeichnis, jeder weitere eine Ebene hoeher.
365
+ let directory: string | null = dirnameOf(context.fromPath);
366
+ for (let step = 1; step < leadingDots; step += 1) {
367
+ directory = directory === null ? null : joinRelative(directory, "..");
368
+ }
369
+ if (directory === null) {
370
+ return unresolved(
371
+ "target_file_not_found",
372
+ `relative python import "${specifier}" from "${context.fromPath}" points above the repository root`,
373
+ );
374
+ }
375
+ const base = segments.length === 0 ? directory : joinRelative(directory, segments.join("/"));
376
+ if (base === null) {
377
+ return unresolved(
378
+ "target_file_not_found",
379
+ `relative python import "${specifier}" from "${context.fromPath}" points above the repository root`,
380
+ );
381
+ }
382
+ const probe = probeCandidates(context, context.fromRepoId, base);
383
+ if (probe.path === null) {
384
+ return unresolved(
385
+ "target_file_not_found",
386
+ `no module file matched relative python import "${specifier}" from "${context.fromPath}"`,
387
+ probe.tried,
388
+ );
389
+ }
390
+ return {
391
+ status: "resolved",
392
+ mechanism: "relative-path",
393
+ targetRepoId: context.fromRepoId,
394
+ targetPath: probe.path,
395
+ };
396
+ }
397
+
398
+ // Absoluter, punktierter Modulpfad: gegen die Repositorywurzel und gegen
399
+ // jedes Paketverzeichnis des Repositories probieren.
400
+ const relativePath = segments.join("/");
401
+ const roots = ["", ...packageDirsOf(context)];
402
+ const tried: string[] = [];
403
+ for (const root of roots) {
404
+ const base = root === "" ? relativePath : joinRelative(root, relativePath);
405
+ if (base === null) continue;
406
+ const probe = probeCandidates(context, context.fromRepoId, base);
407
+ tried.push(...probe.tried);
408
+ if (probe.path !== null) {
409
+ return {
410
+ status: "resolved",
411
+ mechanism: "language-alias",
412
+ targetRepoId: context.fromRepoId,
413
+ targetPath: probe.path,
414
+ };
415
+ }
416
+ }
417
+ return unresolved(
418
+ "package_not_installed",
419
+ `python module "${specifier}" was not found on any module root of repository "${context.fromRepoId}"; it is most likely an installed distribution`,
420
+ tried,
421
+ );
422
+ },
423
+ };
424
+
425
+ function packageDirsOf(context: ResolveContextV1): string[] {
426
+ const dirs = new Set<string>();
427
+ for (const entry of context.packageCatalog.all()) {
428
+ if (entry.repoId !== context.fromRepoId) continue;
429
+ if (entry.packageDir !== "") dirs.add(entry.packageDir);
430
+ if (entry.rootDir !== undefined) dirs.add(entry.rootDir);
431
+ }
432
+ return [...dirs].sort(compareCodepoints);
433
+ }
434
+
435
+ // ---------------------------------------------------------------------------
436
+ // gemeinsame Nachbereitung
437
+ // ---------------------------------------------------------------------------
438
+
439
+ function targetOrFailure(
440
+ matched: SubpathResolution,
441
+ context: ResolveContextV1,
442
+ targetRepoId: string,
443
+ specifier: string,
444
+ mechanism: "package-exports" | "workspace-package" | "language-alias",
445
+ viaPackageName: string,
446
+ ): ResolveResultV1 {
447
+ if (matched.status === "blocked") {
448
+ return unresolved("excluded_by_configuration", `${matched.detail} (specifier "${specifier}")`);
449
+ }
450
+ if (matched.status === "no_match") {
451
+ return unresolved("target_file_not_found", `${matched.detail} (specifier "${specifier}")`);
452
+ }
453
+ const probe = probeCandidates(context, targetRepoId, normalizePath(matched.target));
454
+ if (probe.path === null) {
455
+ return unresolved(
456
+ "target_file_not_found",
457
+ `package "${viaPackageName}" maps "${specifier}" to "${matched.target}", but no such file is indexed`,
458
+ probe.tried,
459
+ );
460
+ }
461
+ return {
462
+ status: "resolved",
463
+ mechanism,
464
+ targetRepoId,
465
+ targetPath: probe.path,
466
+ viaPackageName,
467
+ };
468
+ }
package/src/restore.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Der LESEPFAD des Dateigraphen: aus einem Abbild (`FileGraphSnapshotV1`)
3
+ * wieder einen abfragbaren Graphen machen.
4
+ *
5
+ * WARUM ES DAS BRAUCHT: `untangle index` baut den Graphen und legt ihn ueber
6
+ * `writeGraphSnapshot` ab. Bis hierher gab es nur `FileDependencyGraph.build`
7
+ * — und der braucht die ROHIMPORTE jeder Datei, die im Abbild gar nicht
8
+ * stehen (das Abbild traegt fertige Kanten, keine Spezifizierer mit
9
+ * Quellpositionen). Ein Leser haette den Graphen also neu bauen muessen, was
10
+ * ohne die Quelldateien nicht geht, oder sich eine eigene Traversierung
11
+ * schreiben muessen — die zweite Wahrheit ueber Zyklen und Budgets, die
12
+ * `traverse.ts` gerade verhindert.
13
+ *
14
+ * WAS EIN ZURUECKGELADENER GRAPH KANN UND WAS NICHT — und warum das hier
15
+ * ausdruecklich steht statt in einer Fussnote:
16
+ *
17
+ * KANN: Knoten, Kanten, Metriken, Topologie, Skip-Ledger, Traversierung in
18
+ * beide Richtungen mit Budget, Zyklenmeldung und Abschneidegrund.
19
+ * KANN NICHT: `applyChanges`. Fortschreiben heisst Kanten NEU AUFLOESEN,
20
+ * und dafuer braucht es die Rohimporte samt Quellpositionen. Sie
21
+ * fehlen im Abbild. Der zurueckgeladene Graph bietet die Methode
22
+ * deshalb gar nicht erst an, statt sie mit halber Eingabe falsch
23
+ * auszufuehren (E4).
24
+ */
25
+ import type {
26
+ FileEdgeV1,
27
+ FileGraphMetricsV1,
28
+ FileNodeV1,
29
+ SkipLedgerV1,
30
+ TraversalDirection,
31
+ TraversalLimitsV1,
32
+ } from "@untangle-code/domain";
33
+ import { compareCodepoints } from "@untangle-code/domain";
34
+
35
+ import { compareEdges } from "./edges.js";
36
+ import type { AdjacencyV1 } from "./metrics.js";
37
+ import { buildAdjacency } from "./metrics.js";
38
+ import { normalizePath } from "./paths.js";
39
+ import type { FileGraphNeighborhoodV1, MonotonicClock } from "./traverse.js";
40
+ import { traverseFileGraph } from "./traverse.js";
41
+ import type { FileGraphSnapshotV1, FileTraversalResultV1, WorkspaceTopologyV1 } from "./types.js";
42
+
43
+ /**
44
+ * Ein aus einem Abbild zurueckgeladener Dateigraph — lesend.
45
+ *
46
+ * Die Abfragemethoden heissen wie die von {@link FileDependencyGraph} und
47
+ * antworten dasselbe; das ist Absicht, damit ein Aufrufer nicht zwei
48
+ * Vokabulare lernen muss.
49
+ */
50
+ export class RestoredFileGraph {
51
+ private readonly nodesById: Map<string, FileNodeV1>;
52
+ private readonly edgesById: Map<string, FileEdgeV1>;
53
+ private readonly edgeIdsByFrom: Map<string, string[]>;
54
+ private readonly adjacency: AdjacencyV1;
55
+ private readonly nodeIdsByPath: Map<string, string[]>;
56
+
57
+ constructor(readonly snapshot: FileGraphSnapshotV1) {
58
+ this.nodesById = new Map(snapshot.nodes.map((node) => [node.fileId, node]));
59
+ this.edgesById = new Map(snapshot.edges.map((edge) => [edge.edgeId, edge]));
60
+ this.edgeIdsByFrom = new Map();
61
+ for (const edge of [...snapshot.edges].sort(compareEdges)) {
62
+ const list = this.edgeIdsByFrom.get(edge.fromFileId);
63
+ if (list === undefined) this.edgeIdsByFrom.set(edge.fromFileId, [edge.edgeId]);
64
+ else list.push(edge.edgeId);
65
+ }
66
+ const nodeIds = [...this.nodesById.keys()].sort(compareCodepoints);
67
+ // Aus den KANTEN gebaut, nicht aus `node.dependencies` — dieselbe Quelle,
68
+ // aus der der gebaute Graph seine Nachbarschaft nimmt. Zwei Wege zu
69
+ // derselben Aussage waeren zwei Gelegenheiten, verschieden zu antworten.
70
+ this.adjacency = buildAdjacency(nodeIds, snapshot.edges);
71
+ this.nodeIdsByPath = new Map();
72
+ for (const node of snapshot.nodes) {
73
+ const key = normalizePath(node.filePath);
74
+ const list = this.nodeIdsByPath.get(key);
75
+ if (list === undefined) this.nodeIdsByPath.set(key, [node.fileId]);
76
+ else list.push(node.fileId);
77
+ }
78
+ }
79
+
80
+ get workspaceId(): string {
81
+ return this.snapshot.workspaceId;
82
+ }
83
+
84
+ get graphGeneration(): string {
85
+ return this.snapshot.graphGeneration;
86
+ }
87
+
88
+ get topology(): WorkspaceTopologyV1 {
89
+ return this.snapshot.topology;
90
+ }
91
+
92
+ nodes(): FileNodeV1[] {
93
+ return [...this.nodesById.values()].sort((left, right) =>
94
+ compareCodepoints(left.fileId, right.fileId),
95
+ );
96
+ }
97
+
98
+ edges(): FileEdgeV1[] {
99
+ return [...this.edgesById.values()].sort(compareEdges);
100
+ }
101
+
102
+ node(fileId: string): FileNodeV1 | null {
103
+ return this.nodesById.get(fileId) ?? null;
104
+ }
105
+
106
+ edge(edgeId: string): FileEdgeV1 | null {
107
+ return this.edgesById.get(edgeId) ?? null;
108
+ }
109
+
110
+ /** Kanten, deren Quelle `fileId` ist, in kanonischer Ordnung. */
111
+ edgesFrom(fileId: string): FileEdgeV1[] {
112
+ return (this.edgeIdsByFrom.get(fileId) ?? [])
113
+ .map((edgeId) => this.edgesById.get(edgeId))
114
+ .filter((edge): edge is FileEdgeV1 => edge !== undefined)
115
+ .sort(compareEdges);
116
+ }
117
+
118
+ /**
119
+ * Alle Dateien dieses Pfades — eine LISTE, weil derselbe repo-relative Pfad
120
+ * in mehreren Repositories eines Workspace existieren darf. Wer hier einen
121
+ * einzelnen Knoten zurueckgaebe, muesste heimlich einen auswaehlen.
122
+ */
123
+ nodesAtPath(filePath: string): FileNodeV1[] {
124
+ return (this.nodeIdsByPath.get(normalizePath(filePath)) ?? [])
125
+ .map((fileId) => this.nodesById.get(fileId))
126
+ .filter((node): node is FileNodeV1 => node !== undefined)
127
+ .sort((left, right) => compareCodepoints(left.fileId, right.fileId));
128
+ }
129
+
130
+ metrics(): FileGraphMetricsV1 {
131
+ return this.snapshot.metrics;
132
+ }
133
+
134
+ skipLedger(): SkipLedgerV1 {
135
+ return this.snapshot.skipped;
136
+ }
137
+
138
+ /** Woran haenge ich (vorwaerts, ueber `dependencies`). */
139
+ dependenciesOf(
140
+ fileId: string,
141
+ limits?: Partial<TraversalLimitsV1>,
142
+ now?: MonotonicClock,
143
+ ): FileTraversalResultV1 {
144
+ return this.traverse([fileId], {
145
+ direction: "forward",
146
+ ...(limits === undefined ? {} : { limits }),
147
+ ...(now === undefined ? {} : { now }),
148
+ });
149
+ }
150
+
151
+ /** Wer haengt an mir (rueckwaerts, ueber `dependents`) — die Wirkungsfrage. */
152
+ dependentsOf(
153
+ fileId: string,
154
+ limits?: Partial<TraversalLimitsV1>,
155
+ now?: MonotonicClock,
156
+ ): FileTraversalResultV1 {
157
+ return this.traverse([fileId], {
158
+ direction: "reverse",
159
+ ...(limits === undefined ? {} : { limits }),
160
+ ...(now === undefined ? {} : { now }),
161
+ });
162
+ }
163
+
164
+ traverse(
165
+ rootFileIds: readonly string[],
166
+ options: {
167
+ direction: TraversalDirection;
168
+ limits?: Partial<TraversalLimitsV1>;
169
+ now?: MonotonicClock;
170
+ },
171
+ ): FileTraversalResultV1 {
172
+ const neighborhood: FileGraphNeighborhoodV1 = {
173
+ adjacency: this.adjacency,
174
+ edgeIdBetween: (from, to, direction) => this.edgeIdBetween(from, to, direction),
175
+ };
176
+ return traverseFileGraph(neighborhood, rootFileIds, options);
177
+ }
178
+
179
+ /** Direkte Nachbarn in Traversierungsrichtung, codepoint-sortiert. */
180
+ neighborsOf(fileId: string, direction: TraversalDirection): readonly string[] {
181
+ const map = direction === "forward" ? this.adjacency.forward : this.adjacency.reverse;
182
+ return map.get(fileId) ?? [];
183
+ }
184
+
185
+ private edgeIdBetween(from: string, to: string, direction: TraversalDirection): string | null {
186
+ const source = direction === "forward" ? from : to;
187
+ const target = direction === "forward" ? to : from;
188
+ for (const edgeId of this.edgeIdsByFrom.get(source) ?? []) {
189
+ const edge = this.edgesById.get(edgeId);
190
+ if (edge?.resolution.status === "resolved" && edge.resolution.targetFileId === target) {
191
+ return edgeId;
192
+ }
193
+ }
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /** Kurzform des Zurueckladens. */
199
+ export function restoreFileGraph(snapshot: FileGraphSnapshotV1): RestoredFileGraph {
200
+ return new RestoredFileGraph(snapshot);
201
+ }