@linklabjs/core 0.1.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 (72) hide show
  1. package/README.md +411 -0
  2. package/package.json +48 -0
  3. package/src/api/DomainNode.ts +1433 -0
  4. package/src/api/Graph.ts +271 -0
  5. package/src/api/PathBuilder.ts +247 -0
  6. package/src/api/index.ts +15 -0
  7. package/src/api/loadGraph.ts +207 -0
  8. package/src/api/test-api.ts +153 -0
  9. package/src/api/test-domain.ts +119 -0
  10. package/src/api/types.ts +88 -0
  11. package/src/config/synonyms.json +28 -0
  12. package/src/core/EventBus.ts +187 -0
  13. package/src/core/GraphEvents.ts +153 -0
  14. package/src/core/PathFinder.ts +283 -0
  15. package/src/formatters/BaseFormatter.ts +17 -0
  16. package/src/graph/GraphAssembler.ts +50 -0
  17. package/src/graph/GraphCompiler.ts +412 -0
  18. package/src/graph/GraphExtractor.ts +191 -0
  19. package/src/graph/GraphOptimizer.ts +404 -0
  20. package/src/graph/GraphTrainer.ts +247 -0
  21. package/src/http/LinkBuilder.ts +244 -0
  22. package/src/http/TrailRequest.ts +48 -0
  23. package/src/http/example-netflix.ts +59 -0
  24. package/src/http/hateoas/README.md +87 -0
  25. package/src/http/index.ts +33 -0
  26. package/src/http/plugin.ts +360 -0
  27. package/src/index.ts +121 -0
  28. package/src/instrumentation/TelemetryShim.ts +172 -0
  29. package/src/navigation/NavigationEngine.ts +441 -0
  30. package/src/navigation/Resolver.ts +134 -0
  31. package/src/navigation/Scheduler.ts +136 -0
  32. package/src/navigation/Trail.ts +252 -0
  33. package/src/navigation/TrailParser.ts +207 -0
  34. package/src/navigation/index.ts +11 -0
  35. package/src/providers/MockProvider.ts +68 -0
  36. package/src/providers/PostgresProvider.ts +187 -0
  37. package/src/runtime/CompiledGraphEngine.ts +274 -0
  38. package/src/runtime/DataLoader.ts +236 -0
  39. package/src/runtime/Engine.ts +163 -0
  40. package/src/runtime/QueryEngine.ts +222 -0
  41. package/src/scenarios/test-metro-paris/config.json +6 -0
  42. package/src/scenarios/test-metro-paris/graph.json +16325 -0
  43. package/src/scenarios/test-metro-paris/queries.ts +152 -0
  44. package/src/scenarios/test-metro-paris/stack.json +1 -0
  45. package/src/scenarios/test-musicians/config.json +10 -0
  46. package/src/scenarios/test-musicians/graph.json +20 -0
  47. package/src/scenarios/test-musicians/stack.json +1 -0
  48. package/src/scenarios/test-netflix/MIGRATION.md +23 -0
  49. package/src/scenarios/test-netflix/README.md +138 -0
  50. package/src/scenarios/test-netflix/actions.ts +92 -0
  51. package/src/scenarios/test-netflix/config.json +6 -0
  52. package/src/scenarios/test-netflix/data/categories.json +1 -0
  53. package/src/scenarios/test-netflix/data/companies.json +1 -0
  54. package/src/scenarios/test-netflix/data/credits.json +19797 -0
  55. package/src/scenarios/test-netflix/data/departments.json +18 -0
  56. package/src/scenarios/test-netflix/data/jobs.json +142 -0
  57. package/src/scenarios/test-netflix/data/movies.json +3497 -0
  58. package/src/scenarios/test-netflix/data/people.json +1 -0
  59. package/src/scenarios/test-netflix/data/synonyms.json +8 -0
  60. package/src/scenarios/test-netflix/data/users.json +70 -0
  61. package/src/scenarios/test-netflix/graph.json +1017 -0
  62. package/src/scenarios/test-netflix/queries.ts +159 -0
  63. package/src/scenarios/test-netflix/stack.json +14 -0
  64. package/src/schema/GraphBuilder.ts +106 -0
  65. package/src/schema/JsonSchemaExtractor.ts +107 -0
  66. package/src/schema/SchemaAnalyzer.ts +175 -0
  67. package/src/schema/SchemaExtractor.ts +102 -0
  68. package/src/schema/SynonymResolver.ts +143 -0
  69. package/src/scripts/dictionary.json +796 -0
  70. package/src/scripts/graph.json +664 -0
  71. package/src/scripts/regenerate.ts +248 -0
  72. package/src/types/index.ts +506 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * EventBus — Bus générique pour hooks, events et errors
3
+ *
4
+ * Trois bus distincts, trois contrats clairs :
5
+ *
6
+ * HookBus — awaitable, peut modifier/annuler le flux
7
+ * EventBus — fire-and-forget, observationnel, ne bloque pas
8
+ * ErrorBus — synchrone, jamais silencieux
9
+ *
10
+ * Usage :
11
+ * const bus = new EventBus<MyEvents>()
12
+ * bus.on('traversal.complete', handler)
13
+ * bus.emit('traversal.complete', data)
14
+ * bus.off('traversal.complete', handler)
15
+ */
16
+
17
+ // ── Types de base ─────────────────────────────────────────────
18
+
19
+ export type Handler<T = any> = (data: T) => void
20
+ export type AsyncHandler<T = any, R = T | void> = (data: T) => Promise<R> | R
21
+
22
+ // ── EventBus — fire-and-forget ────────────────────────────────
23
+
24
+ export class EventBus<TEvents extends Record<string, any> = Record<string, any>> {
25
+ private handlers = new Map<string, Set<Handler>>()
26
+
27
+ on<K extends keyof TEvents & string>(event: K, handler: Handler<TEvents[K]>): () => void {
28
+ if (!this.handlers.has(event)) {
29
+ this.handlers.set(event, new Set())
30
+ }
31
+ this.handlers.get(event)!.add(handler)
32
+
33
+ // Retourne une fonction de désinscription
34
+ return () => this.off(event, handler)
35
+ }
36
+
37
+ off<K extends keyof TEvents & string>(event: K, handler: Handler<TEvents[K]>): void {
38
+ this.handlers.get(event)?.delete(handler)
39
+ }
40
+
41
+ emit<K extends keyof TEvents & string>(event: K, data: TEvents[K]): void {
42
+ const handlers = this.handlers.get(event)
43
+ if (!handlers?.size) return
44
+
45
+ // Fire-and-forget — les erreurs dans les handlers ne propagent pas
46
+ for (const handler of handlers) {
47
+ try {
48
+ handler(data)
49
+ } catch (err) {
50
+ console.error(`[EventBus] Handler error on "${event}":`, err)
51
+ }
52
+ }
53
+ }
54
+
55
+ clear(event?: string): void {
56
+ if (event) {
57
+ this.handlers.delete(event)
58
+ } else {
59
+ this.handlers.clear()
60
+ }
61
+ }
62
+
63
+ listenerCount(event: string): number {
64
+ return this.handlers.get(event)?.size ?? 0
65
+ }
66
+ }
67
+
68
+ // ── HookBus — awaitable, peut modifier ou annuler ────────────
69
+
70
+ export interface HookResult<T> {
71
+ value: T
72
+ cancelled?: boolean
73
+ reason?: string
74
+ }
75
+
76
+ export class HookBus<THooks extends Record<string, any> = Record<string, any>> {
77
+ private handlers = new Map<string, AsyncHandler[]>()
78
+
79
+ on<K extends keyof THooks & string>(
80
+ hook: K,
81
+ handler: AsyncHandler<THooks[K]>
82
+ ): () => void {
83
+ if (!this.handlers.has(hook)) {
84
+ this.handlers.set(hook, [])
85
+ }
86
+ this.handlers.get(hook)!.push(handler)
87
+
88
+ return () => this.off(hook, handler)
89
+ }
90
+
91
+ off<K extends keyof THooks & string>(hook: K, handler: AsyncHandler<THooks[K]>): void {
92
+ const list = this.handlers.get(hook)
93
+ if (!list) return
94
+ const idx = list.indexOf(handler)
95
+ if (idx !== -1) list.splice(idx, 1)
96
+ }
97
+
98
+ /**
99
+ * Appelle les handlers en séquence.
100
+ * Chaque handler peut retourner une valeur modifiée — elle est passée au suivant.
101
+ * Si un handler retourne { cancelled: true }, la chaîne s'arrête.
102
+ */
103
+ async call<K extends keyof THooks & string>(
104
+ hook: K,
105
+ data: THooks[K]
106
+ ): Promise<HookResult<THooks[K]>> {
107
+ const handlers = this.handlers.get(hook)
108
+ if (!handlers?.length) return { value: data }
109
+
110
+ let current = data
111
+
112
+ for (const handler of handlers) {
113
+ const result = await handler(current)
114
+
115
+ // Si le handler retourne un objet avec cancelled, on stoppe
116
+ if (result && typeof result === 'object' && 'cancelled' in result && result.cancelled) {
117
+ return { value: current, cancelled: true, reason: result.reason }
118
+ }
119
+
120
+ // Si le handler retourne une valeur, elle remplace le contexte courant
121
+ if (result !== undefined && result !== null) {
122
+ current = result as THooks[K]
123
+ }
124
+ }
125
+
126
+ return { value: current }
127
+ }
128
+
129
+ listenerCount(hook: string): number {
130
+ return this.handlers.get(hook)?.length ?? 0
131
+ }
132
+ }
133
+
134
+ // ── ErrorBus — synchrone, jamais silencieux ──────────────────
135
+
136
+ export class ErrorBus<TErrors extends Record<string, any> = Record<string, any>> {
137
+ private handlers = new Map<string, Set<Handler>>()
138
+ private fallback?: (event: string, data: any) => void
139
+
140
+ /**
141
+ * Handler de fallback si aucun handler n'est enregistré pour cette erreur.
142
+ * Par défaut : console.error.
143
+ */
144
+ setFallback(fn: (event: string, data: any) => void): void {
145
+ this.fallback = fn
146
+ }
147
+
148
+ on<K extends keyof TErrors & string>(event: K, handler: Handler<TErrors[K]>): () => void {
149
+ if (!this.handlers.has(event)) {
150
+ this.handlers.set(event, new Set())
151
+ }
152
+ this.handlers.get(event)!.add(handler)
153
+
154
+ return () => this.off(event, handler)
155
+ }
156
+
157
+ off<K extends keyof TErrors & string>(event: K, handler: Handler<TErrors[K]>): void {
158
+ this.handlers.get(event)?.delete(handler)
159
+ }
160
+
161
+ emit<K extends keyof TErrors & string>(event: K, data: TErrors[K]): void {
162
+ const handlers = this.handlers.get(event)
163
+
164
+ if (!handlers?.size) {
165
+ // Jamais silencieux — fallback ou console.error
166
+ if (this.fallback) {
167
+ this.fallback(event, data)
168
+ } else {
169
+ console.error(`[LinkLab Error] ${event}:`, data)
170
+ }
171
+ return
172
+ }
173
+
174
+ for (const handler of handlers) {
175
+ try {
176
+ handler(data)
177
+ } catch (err) {
178
+ // Les erreurs dans les error handlers ne doivent jamais être avalées
179
+ console.error(`[ErrorBus] Handler threw on "${event}":`, err)
180
+ }
181
+ }
182
+ }
183
+
184
+ listenerCount(event: string): number {
185
+ return this.handlers.get(event)?.size ?? 0
186
+ }
187
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * GraphEvents — Catalogue des événements LinkLab
3
+ *
4
+ * Trois bus, trois contrats :
5
+ *
6
+ * graph.hooks — awaitable, enrichit ou annule le flux
7
+ * graph.events — fire-and-forget, observation pure
8
+ * graph.errors — synchrone, jamais silencieux
9
+ *
10
+ * Conventions de nommage :
11
+ * hooks : <sujet>.<moment> ex: traversal.before, access.check
12
+ * events : <sujet>.<résultat> ex: traversal.complete, cache.miss
13
+ * errors : <sujet>.<type> ex: route.notfound, traversal.failed
14
+ */
15
+
16
+ import { HookBus, EventBus, ErrorBus } from './EventBus.js'
17
+ import type { Frame, GraphEdge, NavigationPath, Graph } from '../types/index.js'
18
+
19
+ // ── Payloads Hooks ────────────────────────────────────────────
20
+
21
+ export interface TraversalBeforePayload {
22
+ from: string
23
+ to: string
24
+ stack: Frame[]
25
+ graph: Graph
26
+ }
27
+
28
+ export interface TraversalStepPayload {
29
+ node: string
30
+ edge: GraphEdge
31
+ stack: Frame[]
32
+ depth: number
33
+ }
34
+
35
+ export interface AccessCheckPayload {
36
+ node: string
37
+ stack: Frame[]
38
+ context?: Record<string, any>
39
+ }
40
+
41
+ export interface StackPushPayload {
42
+ frame: Frame
43
+ stack: Frame[]
44
+ }
45
+
46
+ export interface StackPopPayload {
47
+ frame: Frame
48
+ stack: Frame[]
49
+ }
50
+
51
+ // ── Payloads Events ───────────────────────────────────────────
52
+
53
+ export interface TraversalCompletePayload {
54
+ from: string
55
+ to: string
56
+ path: NavigationPath
57
+ durationMs: number
58
+ stackDepth: number
59
+ routeUsed: string // ex: "movies→credits→people"
60
+ routeWeight: number
61
+ resultCount?: number
62
+ }
63
+
64
+ export interface CacheMissPayload {
65
+ key: string
66
+ requestedAt: number
67
+ }
68
+
69
+ export interface CacheHitPayload {
70
+ key: string
71
+ accessCount: number
72
+ cachedAt?: number
73
+ }
74
+
75
+ export interface WeightUpdatedPayload {
76
+ edge: string // "from→to"
77
+ previousWeight: number
78
+ newWeight: number
79
+ reason?: string
80
+ }
81
+
82
+ export interface StackCompactedPayload {
83
+ before: Frame[]
84
+ after: Frame[]
85
+ removedCount: number
86
+ }
87
+
88
+ // ── Payloads Errors ───────────────────────────────────────────
89
+
90
+ export interface RouteNotFoundPayload {
91
+ from: string
92
+ to: string
93
+ stack: Frame[]
94
+ }
95
+
96
+ export interface TraversalFailedPayload {
97
+ from: string
98
+ to: string
99
+ reason: string
100
+ error?: Error
101
+ }
102
+
103
+ export interface HookTimeoutPayload {
104
+ hook: string
105
+ timeoutMs: number
106
+ }
107
+
108
+ export interface AccessDeniedPayload {
109
+ node: string
110
+ reason: string
111
+ stack: Frame[]
112
+ }
113
+
114
+ // ── Registres typés ───────────────────────────────────────────
115
+
116
+ export interface GraphHooks {
117
+ 'traversal.before': TraversalBeforePayload
118
+ 'traversal.step': TraversalStepPayload
119
+ 'access.check': AccessCheckPayload
120
+ 'stack.push': StackPushPayload
121
+ 'stack.pop': StackPopPayload
122
+ }
123
+
124
+ export interface GraphEventMap {
125
+ 'traversal.complete': TraversalCompletePayload
126
+ 'cache.miss': CacheMissPayload
127
+ 'cache.hit': CacheHitPayload
128
+ 'weight.updated': WeightUpdatedPayload
129
+ 'stack.compacted': StackCompactedPayload
130
+ }
131
+
132
+ export interface GraphErrors {
133
+ 'route.notfound': RouteNotFoundPayload
134
+ 'traversal.failed': TraversalFailedPayload
135
+ 'hook.timeout': HookTimeoutPayload
136
+ 'access.denied': AccessDeniedPayload
137
+ }
138
+
139
+ // ── Factory — crée les trois bus pour une instance de graphe ──
140
+
141
+ export interface GraphBuses {
142
+ hooks: HookBus<GraphHooks>
143
+ events: EventBus<GraphEventMap>
144
+ errors: ErrorBus<GraphErrors>
145
+ }
146
+
147
+ export function createGraphBuses(): GraphBuses {
148
+ return {
149
+ hooks: new HookBus<GraphHooks>(),
150
+ events: new EventBus<GraphEventMap>(),
151
+ errors: new ErrorBus<GraphErrors>(),
152
+ }
153
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * PathFinder - Dijkstra + DFS limité
3
+ *
4
+ * Deux algorithmes selon l'usage :
5
+ *
6
+ * findShortestPath() → Dijkstra (chemin optimal garanti, performant)
7
+ * findAllPaths() → DFS limité (N meilleurs chemins alternatifs)
8
+ *
9
+ * Sur un graphe de métro (300+ stations, 900+ arêtes),
10
+ * le DFS pur explose. Dijkstra est l'algorithme correct.
11
+ */
12
+
13
+ import type { Graph, GraphEdge, Path, PathDetails } from '../types/index.js'
14
+
15
+ export class PathFinder {
16
+ private adjacencyList: Map<string, Array<{ to: string; edge: GraphEdge }>>
17
+
18
+ constructor(private graph: Graph) {
19
+ this.adjacencyList = this.buildAdjacencyList(graph)
20
+ }
21
+
22
+ // ==================== DIJKSTRA ====================
23
+
24
+ /**
25
+ * Chemin le plus court par poids (Dijkstra).
26
+ * Garanti optimal. Performant sur grands graphes.
27
+ */
28
+ findShortestPath(from: string, to: string): PathDetails | null {
29
+ const dist = new Map<string, number>()
30
+ const prev = new Map<string, { node: string; edge: GraphEdge } | null>()
31
+ const visited = new Set<string>()
32
+
33
+ // Initialisation
34
+ for (const node of this.graph.nodes) {
35
+ dist.set(node.id, Infinity)
36
+ }
37
+ dist.set(from, 0)
38
+ prev.set(from, null)
39
+
40
+ // Priority queue simple (pour notre taille, suffisant)
41
+ const queue = new Set<string>(this.graph.nodes.map(n => n.id))
42
+
43
+ while (queue.size > 0) {
44
+ // Nœud non visité avec distance minimale
45
+ let u: string | null = null
46
+ let minDist = Infinity
47
+ for (const node of queue) {
48
+ const d = dist.get(node) ?? Infinity
49
+ if (d < minDist) {
50
+ minDist = d
51
+ u = node
52
+ }
53
+ }
54
+
55
+ if (u === null || u === to) break
56
+ if (minDist === Infinity) break // Graphe non connexe
57
+
58
+ queue.delete(u)
59
+ visited.add(u)
60
+
61
+ const neighbors = this.adjacencyList.get(u) ?? []
62
+ for (const { to: v, edge } of neighbors) {
63
+ if (visited.has(v)) continue
64
+
65
+ const alt = (dist.get(u) ?? Infinity) + edge.weight
66
+ if (alt < (dist.get(v) ?? Infinity)) {
67
+ dist.set(v, alt)
68
+ prev.set(v, { node: u, edge })
69
+ }
70
+ }
71
+ }
72
+
73
+ if (!prev.has(to) && to !== from) return null
74
+ if ((dist.get(to) ?? Infinity) === Infinity) return null
75
+
76
+ // Reconstruction du chemin
77
+ const path: string[] = []
78
+ const edges: GraphEdge[] = []
79
+ let current: string | null = to
80
+
81
+ while (current !== null) {
82
+ path.unshift(current)
83
+ const p = prev.get(current)
84
+ if (p) {
85
+ edges.unshift(p.edge)
86
+ current = p.node
87
+ } else {
88
+ current = null
89
+ }
90
+ }
91
+
92
+ return {
93
+ path,
94
+ edges,
95
+ length: path.length,
96
+ joins: path.length - 1,
97
+ weight: dist.get(to) ?? 0,
98
+ indirect: path.length > 2
99
+ }
100
+ }
101
+
102
+ /**
103
+ * N meilleurs chemins (Yen's K-shortest paths simplifié).
104
+ * Trouve le plus court via Dijkstra, puis des alternatives
105
+ * en pénalisant les arêtes du chemin précédent.
106
+ */
107
+ findAllPaths(from: string, to: string, maxPaths = 3, _maxDepth = 50, transferPenalty = 0, allowedVia?: string[], minHops = 0): Path[] {
108
+ const results: PathDetails[] = []
109
+ const penalized = new Set<string>() // arêtes temporairement exclues
110
+
111
+ for (let k = 0; k < maxPaths; k++) {
112
+ const result = this.dijkstraWithExclusions(from, to, penalized, transferPenalty, allowedVia, minHops)
113
+ if (!result) break
114
+
115
+ results.push(result)
116
+
117
+ // Pénaliser la dernière arête du chemin trouvé pour forcer une alternative
118
+ if (result.edges.length > 0) {
119
+ const lastEdge = result.edges[result.edges.length - 1]
120
+ penalized.add(lastEdge.name ?? `${lastEdge.from}->${lastEdge.to}`)
121
+ }
122
+ }
123
+
124
+ return results.map(r => r.path)
125
+ }
126
+
127
+ /**
128
+ * Dijkstra avec exclusion d'arêtes (pour les chemins alternatifs)
129
+ */
130
+ private dijkstraWithExclusions(
131
+ from: string,
132
+ to: string,
133
+ excluded: Set<string>,
134
+ transferPenalty = 0,
135
+ allowedVia?: string[],
136
+ minHops = 0
137
+ ): PathDetails | null {
138
+ const dist = new Map<string, number>()
139
+ const prev = new Map<string, { node: string; edge: GraphEdge } | null>()
140
+ const visited = new Set<string>()
141
+
142
+ for (const node of this.graph.nodes) dist.set(node.id, Infinity)
143
+ dist.set(from, 0)
144
+ prev.set(from, null)
145
+
146
+ const queue = new Set<string>(this.graph.nodes.map(n => n.id))
147
+
148
+ while (queue.size > 0) {
149
+ let u: string | null = null
150
+ let minDist = Infinity
151
+ for (const node of queue) {
152
+ const d = dist.get(node) ?? Infinity
153
+ if (d < minDist) { minDist = d; u = node }
154
+ }
155
+
156
+ if (u === null || u === to || minDist === Infinity) break
157
+
158
+ queue.delete(u)
159
+ visited.add(u)
160
+
161
+ for (const { to: v, edge } of this.adjacencyList.get(u) ?? []) {
162
+ if (visited.has(v)) continue
163
+ const edgeKey = edge.name ?? `${edge.from}->${edge.to}`
164
+ if (excluded.has(edgeKey)) continue
165
+
166
+ // Filtre via — si spécifié, ignorer les arêtes dont le type n'est pas dans la liste
167
+ if (allowedVia && allowedVia.length > 0) {
168
+ const edgeType = edge.metadata?.type ?? edge.via
169
+ if (!allowedVia.includes(edgeType)) continue
170
+ }
171
+
172
+ // Pénalité sur changement de ligne — deux cas :
173
+ // 1. Arête explicitement TRANSFER (self-loop de correspondance)
174
+ // 2. Changement de ligne implicite (arête DIRECT mais ligne différente de la précédente)
175
+ let penalty = 0
176
+ if (transferPenalty > 0) {
177
+ const isExplicitTransfer = edge.metadata?.type === 'TRANSFER'
178
+ const prevEdge = prev.get(u)
179
+ const prevLineId = prevEdge?.edge?.metadata?.lineId
180
+ const currLineId = edge.metadata?.lineId
181
+ const isLineChange = prevLineId && currLineId && prevLineId !== currLineId
182
+ && edge.metadata?.type !== 'TRANSFER'
183
+ if (isExplicitTransfer || isLineChange) {
184
+ penalty = transferPenalty
185
+ }
186
+ }
187
+ const alt = (dist.get(u) ?? Infinity) + edge.weight + penalty
188
+ if (alt < (dist.get(v) ?? Infinity)) {
189
+ dist.set(v, alt)
190
+ prev.set(v, { node: u, edge })
191
+ }
192
+ }
193
+ }
194
+
195
+ if ((dist.get(to) ?? Infinity) === Infinity) return null
196
+
197
+ const path: string[] = []
198
+ const edges: GraphEdge[] = []
199
+ let current: string | null = to
200
+
201
+ while (current !== null) {
202
+ path.unshift(current)
203
+ const p = prev.get(current)
204
+ if (p) { edges.unshift(p.edge); current = p.node }
205
+ else current = null
206
+ }
207
+
208
+ // Filtre minHops — rejeter les chemins trop courts
209
+ if (minHops > 0 && path.length - 1 < minHops) return null
210
+
211
+ return {
212
+ path, edges,
213
+ length: path.length,
214
+ joins: path.length - 1,
215
+ weight: dist.get(to) ?? 0,
216
+ indirect: path.length > 2
217
+ }
218
+ }
219
+
220
+ // ==================== HELPERS ====================
221
+
222
+ getPathWeight(path: Path): number {
223
+ let total = 0
224
+ for (let i = 0; i < path.length - 1; i++) {
225
+ const edge = this.graph.edges.find(e => e.from === path[i] && e.to === path[i + 1])
226
+ if (edge) total += edge.weight
227
+ }
228
+ return total
229
+ }
230
+
231
+ getPathDetails(path: Path): PathDetails {
232
+ const edges: GraphEdge[] = []
233
+ for (let i = 0; i < path.length - 1; i++) {
234
+ const edge = this.graph.edges.find(e => e.from === path[i] && e.to === path[i + 1])
235
+ if (edge) edges.push(edge)
236
+ }
237
+ return {
238
+ path, edges,
239
+ length: path.length,
240
+ joins: path.length - 1,
241
+ weight: this.getPathWeight(path),
242
+ indirect: path.length > 2
243
+ }
244
+ }
245
+
246
+ hasPath(from: string, to: string): boolean {
247
+ return this.findShortestPath(from, to) !== null
248
+ }
249
+
250
+ getReachableNodes(from: string, maxDepth = 50): Set<string> {
251
+ const reachable = new Set<string>()
252
+ const visited = new Set<string>()
253
+ const dfs = (node: string, depth: number) => {
254
+ if (depth > maxDepth || visited.has(node)) return
255
+ visited.add(node)
256
+ reachable.add(node)
257
+ for (const { to } of this.adjacencyList.get(node) ?? []) dfs(to, depth + 1)
258
+ }
259
+ dfs(from, 0)
260
+ reachable.delete(from)
261
+ return reachable
262
+ }
263
+
264
+ private buildAdjacencyList(graph: Graph): Map<string, Array<{ to: string; edge: GraphEdge }>> {
265
+ const adj = new Map<string, Array<{ to: string; edge: GraphEdge }>>()
266
+ for (const edge of graph.edges) {
267
+ if (!adj.has(edge.from)) adj.set(edge.from, [])
268
+ adj.get(edge.from)!.push({ to: edge.to, edge })
269
+ if (!adj.has(edge.to)) adj.set(edge.to, [])
270
+ }
271
+ return adj
272
+ }
273
+
274
+ getStats() {
275
+ const degrees = new Map<string, number>()
276
+ for (const node of this.graph.nodes) degrees.set(node.id, 0)
277
+ for (const edge of this.graph.edges) {
278
+ degrees.set(edge.from, (degrees.get(edge.from) ?? 0) + 1)
279
+ }
280
+ const avgDegree = Array.from(degrees.values()).reduce((s, d) => s + d, 0) / degrees.size
281
+ return { nodes: this.graph.nodes.length, edges: this.graph.edges.length, avgDegree }
282
+ }
283
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * BaseFormatter - Interface pour les formatters de sortie
3
+ *
4
+ * Chaque scénario peut avoir son propre formatter.
5
+ * Le formatter transforme un résultat brut du NavigationEngine
6
+ * en sortie lisible par un humain.
7
+ */
8
+
9
+ import type { NavigationPath, EngineStepResult } from '../types/index.js'
10
+
11
+ export interface PathFormatter {
12
+ /** Formate un chemin pour l'affichage humain */
13
+ format(path: NavigationPath): string
14
+
15
+ /** Formate un step complet de résultat */
16
+ formatResult(result: EngineStepResult): string
17
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * GraphAssembler — Dictionary → Graph V3
3
+ *
4
+ * Transforme le Dictionary produit par GraphBuilder
5
+ * en Graph V3 (nodes + edges) prêt pour PathFinder.
6
+ *
7
+ * Corrige le bug d'itération : dictionary.tables est une Table[]
8
+ * (liste), pas un Record<string, Table> (dictionnaire).
9
+ */
10
+
11
+ import type { Dictionary, Graph, GraphNode, GraphEdge } from '../types/index.js'
12
+
13
+ export class GraphAssembler {
14
+
15
+ assemble(dictionary: Dictionary): Graph {
16
+ const nodes: GraphNode[] = []
17
+ const edges: GraphEdge[] = []
18
+
19
+ // 1. Tables → Nodes
20
+ // dictionary.tables est une Table[] — itération sur les éléments, pas les indices
21
+ for (const table of dictionary.tables) {
22
+ nodes.push({
23
+ id: table.name,
24
+ type: 'table',
25
+ rowCount: table.rowCount,
26
+ columns: table.columns.map(c => ({ name: c, type: 'string' }))
27
+ })
28
+ }
29
+
30
+ // 2. Relations → Edges
31
+ for (const rel of dictionary.relations) {
32
+ edges.push({
33
+ name: rel.label,
34
+ from: rel.from,
35
+ to: rel.to,
36
+ via: rel.via,
37
+ weight: typeof rel.weight === 'string'
38
+ ? parseFloat(rel.weight)
39
+ : (rel.weight ?? 1),
40
+ metadata: {
41
+ type: rel.type,
42
+ condition: rel.condition,
43
+ metadataField: rel.metadataField
44
+ }
45
+ })
46
+ }
47
+
48
+ return { nodes, edges }
49
+ }
50
+ }