@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,271 @@
1
+ /**
2
+ * api/Graph.ts — Niveau 0 : constructeur et surface unifiée
3
+ *
4
+ * Point d'entrée unique de LinkLab.
5
+ *
6
+ * const graph = new Graph(graphJson)
7
+ * const graph = new Graph(graphJson, { compiled, dataset })
8
+ *
9
+ * Expose :
10
+ * Niveau 2 — graph.from(a).to(b).path(strategy)
11
+ * Niveau 3 — graph.entities, graph.relations, graph.weights, graph.schema
12
+ * Niveau 4 — graph.compile(), graph.snapshot()
13
+ *
14
+ * Le niveau 1 (DomainProxy) sera ajouté dans graph.domain() — prochaine étape.
15
+ */
16
+
17
+ import { createRequire } from 'module'
18
+ import fs from 'fs'
19
+ import path from 'path'
20
+ import type {
21
+ Graph as GraphData,
22
+ CompiledGraph,
23
+ GraphNode,
24
+ GraphEdge,
25
+ Provider,
26
+ } from '../types/index.js'
27
+ import { GraphCompiler } from '../graph/GraphCompiler.js'
28
+ import { PathBuilder } from './PathBuilder.js'
29
+ import { createDomain } from './DomainNode.js'
30
+ import type { PathBuilderOptions } from './types.js'
31
+
32
+ export interface GraphOptions {
33
+ /** CompiledGraph précalculé — active .execute() sur PathBuilder */
34
+ compiled?: CompiledGraph
35
+ /** Dataset en mémoire { tableName: rows[] } — active .execute() */
36
+ dataset?: Record<string, any[]>
37
+ /** Provider externe (PostgreSQL, etc.) — active .execute() via SQL */
38
+ provider?: Provider
39
+ /** Préfixe de chemin pour résoudre les imports relatifs */
40
+ basePath?: string
41
+ /** Dictionnaire résolu — labels humains des routes */
42
+ dictionary?: Record<string, any> | null
43
+ }
44
+
45
+ /** Lien navigable depuis un nœud — retourné par linksFrom() */
46
+ export interface NavigationLink {
47
+ to: string // nœud cible
48
+ label: string // nom de la relation ou vue sémantique
49
+ semantic: boolean // true = vue filtrée (actor, director...), false = table physique
50
+ weight?: number // poids courant
51
+ }
52
+
53
+ export class Graph {
54
+ private _data: GraphData
55
+ private _compiled: CompiledGraph | null
56
+ private _dataset: Record<string, any[]> | null
57
+ private _provider: Provider | null
58
+ private _dictionary: Record<string, any> | null
59
+
60
+ constructor(source: GraphData | string, options: GraphOptions = {}) {
61
+ // Accepte un objet GraphData directement ou un chemin vers graph.json
62
+ if (typeof source === 'string') {
63
+ const resolved = options.basePath
64
+ ? path.resolve(options.basePath, source)
65
+ : path.resolve(source)
66
+ const req = createRequire(import.meta.url)
67
+ this._data = req(resolved) as GraphData
68
+ } else {
69
+ this._data = source
70
+ }
71
+
72
+ this._compiled = options.compiled ?? null
73
+ this._dataset = options.dataset ?? null
74
+ this._provider = options.provider ?? null
75
+ this._dictionary = options.dictionary ?? null
76
+ }
77
+
78
+ // ── Niveau 1 — Navigation sémantique (domain proxy) ─────────────────────────
79
+
80
+ /**
81
+ * domain() — retourne un Proxy sémantique sur le graphe.
82
+ * Optionnel — Graph lui-même est utilisable comme domaine directement.
83
+ *
84
+ * const cinema = new Graph(source, opts)
85
+ * await cinema.movies // via Graph comme domaine
86
+ * await cinema.domain().movies // équivalent explicite
87
+ * await cinema.domain('cinema').movies // avec nom (futur: permissions/projections)
88
+ */
89
+ domain(_name?: string) {
90
+ return createDomain({
91
+ graphData: this._data,
92
+ compiled: this._compiled,
93
+ dataset: this._dataset,
94
+ provider: this._provider,
95
+ dictionary: this._dictionary,
96
+ }, this) // ← passe le Graph pour .graph
97
+ }
98
+
99
+ // ── Niveau 2 — Exploration algorithmique ──────────────────────────────────
100
+
101
+ /**
102
+ * from(node) — point de départ d'une traversée.
103
+ *
104
+ * graph.from('Pigalle').to('Alesia').path(Strategy.Comfort())
105
+ * graph.from('movies').to('people').execute({ id: 278 })
106
+ */
107
+ from(node: string, opts: PathBuilderOptions = {}): PathBuilder {
108
+ return new PathBuilder(node, this._data, this._compiled, this._dataset ?? null, opts, this._provider)
109
+ }
110
+
111
+ /**
112
+ * within(node, depth) — exploration radiale depuis un node.
113
+ * Retourne tous les nodes accessibles en ≤ depth sauts.
114
+ *
115
+ * graph.within('Châtelet', 3).nodes
116
+ */
117
+ within(node: string, depth = 2): { nodes: GraphNode[] } {
118
+ const finder = new (require('../core/PathFinder.js').PathFinder)(this._data)
119
+ const reached = finder.getReachableNodes(node, depth)
120
+ const nodes = this._data.nodes.filter(n => reached.has(n.id))
121
+ return { nodes }
122
+ }
123
+
124
+ // ── Niveau 3 — Introspection ───────────────────────────────────────────────
125
+
126
+ /** Liste des entités (nodes) du graphe */
127
+ get entities(): GraphNode[] {
128
+ return [...this._data.nodes]
129
+ }
130
+
131
+ /** Liste des relations (arêtes) du graphe */
132
+ get relations(): GraphEdge[] {
133
+ return [...this._data.edges]
134
+ }
135
+
136
+ /** Poids courants de toutes les arêtes — { edgeName: weight } */
137
+ get weights(): Record<string, number> {
138
+ return Object.fromEntries(
139
+ this._data.edges
140
+ .filter(e => e.name)
141
+ .map(e => [e.name!, Number(e.weight) || 1])
142
+ )
143
+ }
144
+
145
+ /** Schéma résolu — nodes groupés par type */
146
+ get schema(): Record<string, GraphNode[]> {
147
+ const result: Record<string, GraphNode[]> = {}
148
+ for (const node of this._data.nodes) {
149
+ const type = (node as any).type ?? 'node'
150
+ if (!result[type]) result[type] = []
151
+ result[type].push(node)
152
+ }
153
+ return result
154
+ }
155
+
156
+ /**
157
+ * linksFrom(nodeId) — liens navigables depuis un nœud, au niveau sémantique maximal.
158
+ *
159
+ * Retourne les routes physiques ET les vues sémantiques du compiledGraph.
160
+ * Utilisé par : REPL (autocomplétion), TUI, extension VSCode.
161
+ *
162
+ * graph.linksFrom('movies')
163
+ * // → [
164
+ * // { to: 'people', label: 'people', semantic: false }, ← table physique
165
+ * // { to: 'people', label: 'actor', semantic: true }, ← vue filtrée jobId=1
166
+ * // { to: 'people', label: 'director', semantic: true }, ← vue filtrée jobId=2
167
+ * // ]
168
+ */
169
+ linksFrom(nodeId: string): NavigationLink[] {
170
+ const links: NavigationLink[] = []
171
+ const seen = new Set<string>()
172
+
173
+ // Routes physiques — priorité : raw-graph edges, fallback : compiled.routes non-sémantiques
174
+ const rawEdges = this._data.edges.filter(e => e.from === nodeId)
175
+ if (rawEdges.length > 0) {
176
+ for (const e of rawEdges) {
177
+ const label = e.name ?? e.to
178
+ if (!seen.has(label)) {
179
+ seen.add(label)
180
+ links.push({ to: e.to, label, semantic: false, weight: Number(e.weight) || 1 })
181
+ }
182
+ }
183
+ } else if (this._compiled) {
184
+ // Pas de raw-graph (cas loadGraph({ compiled })) → routes physiques depuis compiled
185
+ for (const r of (this._compiled as any).routes ?? []) {
186
+ if (r.from === nodeId && !r.semantic) {
187
+ const label = r.to
188
+ if (!seen.has(label)) {
189
+ seen.add(label)
190
+ links.push({ to: r.to, label, semantic: false, weight: r.primary?.weight })
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ // Routes sémantiques depuis le compiledGraph
197
+ if (this._compiled) {
198
+ for (const r of (this._compiled as any).routes ?? []) {
199
+ if (r.from === nodeId && r.semantic === true) {
200
+ const label = r.label ?? r.to
201
+ if (!seen.has(label)) {
202
+ seen.add(label)
203
+ links.push({ to: r.to, label, semantic: true, weight: r.primary?.weight })
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ return links
210
+ }
211
+
212
+ // ── Niveau 4 — Maintenance ─────────────────────────────────────────────────
213
+
214
+ /**
215
+ * compile() — précalcule les routes optimales.
216
+ * Retourne un nouveau Graph avec le compiledGraph injecté.
217
+ */
218
+ compile(config: Parameters<GraphCompiler['compile']>[1] = new Map()): Graph {
219
+ const compiler = new GraphCompiler()
220
+ const compiled = compiler.compile(this._data, config)
221
+ return new Graph(this._data, {
222
+ compiled,
223
+ dataset: this._dataset ?? undefined,
224
+ })
225
+ }
226
+
227
+ /**
228
+ * snapshot() — sérialise l'état courant (graph + compiled si présent).
229
+ */
230
+ snapshot(): { graph: GraphData; compiled: CompiledGraph | null } {
231
+ return {
232
+ graph: this._data,
233
+ compiled: this._compiled,
234
+ }
235
+ }
236
+
237
+ /**
238
+ * weight(edgeName).set(value) — ajuste le poids d'une arête.
239
+ * Retourne un nouveau Graph (immuable).
240
+ */
241
+ weight(edgeName: string) {
242
+ return {
243
+ set: (value: number): Graph => {
244
+ const edges = this._data.edges.map(e =>
245
+ e.name === edgeName ? { ...e, weight: value } : e
246
+ )
247
+ return new Graph(
248
+ { ...this._data, edges },
249
+ { compiled: this._compiled ?? undefined, dataset: this._dataset ?? undefined }
250
+ )
251
+ },
252
+ update: (fn: (current: number) => number): Graph => {
253
+ const edges = this._data.edges.map(e => {
254
+ if (e.name !== edgeName) return e
255
+ return { ...e, weight: fn(Number(e.weight) || 1) }
256
+ })
257
+ return new Graph(
258
+ { ...this._data, edges },
259
+ { compiled: this._compiled ?? undefined, dataset: this._dataset ?? undefined }
260
+ )
261
+ },
262
+ }
263
+ }
264
+
265
+ // ── Accès aux données brutes ───────────────────────────────────────────────
266
+
267
+ /** GraphData interne — pour les couches qui en ont besoin */
268
+ get raw(): GraphData {
269
+ return this._data
270
+ }
271
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * api/PathBuilder.ts — Niveau 2 : exploration algorithmique
3
+ *
4
+ * Point d'entrée : graph.from(a).to(b)
5
+ *
6
+ * Surface publique :
7
+ * .path(strategy?) → meilleur chemin (PathResult)
8
+ * .paths(strategy?) → tous les chemins ordonnés (PathResult)
9
+ * .links → graphe de relations entre les deux nodes
10
+ * .execute(filters) → traversée avec hydratation de données
11
+ *
12
+ * Compile vers PathFinder (Dijkstra) + QueryEngine (données).
13
+ * Ne connaît pas le domaine — c'est le niveau 1 (DomainProxy) qui traduit
14
+ * les noms sémantiques en IDs de nodes avant d'appeler PathBuilder.
15
+ */
16
+
17
+ import type { Graph, CompiledGraph, GraphEdge, Provider } from '../types/index.js'
18
+ import { PathFinder } from '../core/PathFinder.js'
19
+ import { QueryEngine } from '../runtime/QueryEngine.js'
20
+ import type {
21
+ Strategy,
22
+ PathResult,
23
+ ResolvedPath,
24
+ PathStep,
25
+ QueryResult,
26
+ PathBuilderOptions,
27
+ } from './types.js'
28
+ import { Strategy as S } from './types.js'
29
+
30
+ export class PathBuilder {
31
+ private _from: string
32
+ private _to: string | null = null
33
+ private _opts: PathBuilderOptions
34
+ private _graph: Graph
35
+ private _compiled: CompiledGraph | null
36
+ private _dataset: Record<string, any[]> | null
37
+ private _provider: Provider | null
38
+
39
+ constructor(
40
+ from: string,
41
+ graph: Graph,
42
+ compiled: CompiledGraph | null = null,
43
+ dataset: Record<string, any[]> | null = null,
44
+ opts: PathBuilderOptions = {},
45
+ provider: Provider | null = null
46
+ ) {
47
+ this._from = from
48
+ this._graph = graph
49
+ this._compiled = compiled
50
+ this._dataset = dataset
51
+ this._opts = opts
52
+ this._provider = provider
53
+ }
54
+
55
+ // ── Builder ────────────────────────────────────────────────────────────────
56
+
57
+ to(node: string): this {
58
+ this._to = node
59
+ return this
60
+ }
61
+
62
+ maxPaths(n: number): this {
63
+ this._opts = { ...this._opts, maxPaths: n }
64
+ return this
65
+ }
66
+
67
+ via(edgeTypes: string[]): this {
68
+ this._opts = { ...this._opts, via: edgeTypes }
69
+ return this
70
+ }
71
+
72
+ minHops(n: number): this {
73
+ this._opts = { ...this._opts, minHops: n }
74
+ return this
75
+ }
76
+
77
+ // ── Résultats ──────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * path(strategy?) — meilleur chemin selon la stratégie.
81
+ * Stratégie par défaut : Shortest (poids brut).
82
+ *
83
+ * metro: graph.from('Pigalle').to('Alesia').path(Strategy.Comfort())
84
+ * musicians: graph.from('Jackson').to('West').path()
85
+ */
86
+ path(strategy?: Strategy): PathResult {
87
+ return this._findPaths(1, strategy)
88
+ }
89
+
90
+ /**
91
+ * paths(strategy?) — tous les chemins ordonnés par poids.
92
+ *
93
+ * metro: graph.from('Chatelet').to('Nation').paths(Strategy.Shortest())
94
+ * musicians: graph.from('Pharrell').to('Kanye').paths()
95
+ */
96
+ paths(strategy?: Strategy): PathResult {
97
+ const maxPaths = this._opts.maxPaths ?? 5
98
+ return this._findPaths(maxPaths, strategy)
99
+ }
100
+
101
+ /**
102
+ * links — graphe de relations entre from et to.
103
+ * Retourne toutes les arêtes qui participent aux chemins possibles,
104
+ * sans les ordonner — vue structurelle, pas navigationnelle.
105
+ *
106
+ * musicians: graph.from('Jackson').to('West').links
107
+ */
108
+ get links(): PathResult & { edges: GraphEdge[] } {
109
+ const result = this._findPaths(this._opts.maxPaths ?? 10)
110
+ // Collecter toutes les arêtes participant aux chemins trouvés
111
+ const nodeSet = new Set(result.paths.flatMap(p => p.nodes))
112
+ const edges = this._graph.edges.filter(
113
+ e => nodeSet.has(e.from) && nodeSet.has(e.to)
114
+ )
115
+ return { ...result, edges }
116
+ }
117
+
118
+ /**
119
+ * execute(filters) — traversée avec hydratation de données.
120
+ * Uniquement disponible si un dataset ou provider est configuré.
121
+ *
122
+ * netflix: graph.from('movies').to('people').execute({ id: 278 })
123
+ * dvdrental: graph.from('customer').to('actor').execute({ id: 1 })
124
+ */
125
+ async execute<T = Record<string, any>>(
126
+ filters: Record<string, any> = {}
127
+ ): Promise<QueryResult<T>> {
128
+ if (!this._to) throw new Error(`PathBuilder : .to(node) requis avant execute()`)
129
+ const to = this._to
130
+ const start = Date.now()
131
+
132
+ if (!this._compiled || !this._dataset) {
133
+ throw new Error(
134
+ `execute() nécessite un compiledGraph et un dataset.\n` +
135
+ `Utilisez new Graph(graphJson, { compiled, dataset }) pour activer la traversée de données.`
136
+ )
137
+ }
138
+
139
+ const engine = new QueryEngine(this._compiled)
140
+ const data = engine.executeInMemory(
141
+ { from: this._from, to, filters },
142
+ this._dataset
143
+ ) as T[]
144
+
145
+ let path: string[] = [this._from, to]
146
+ try {
147
+ const route = engine.getRoute(this._from, to)
148
+ path = route.primary.path
149
+ } catch { /* route inconnue — chemin direct */ }
150
+
151
+ return {
152
+ from: this._from,
153
+ to,
154
+ filters,
155
+ data,
156
+ path,
157
+ timing: Date.now() - start,
158
+ }
159
+ }
160
+
161
+ // ── Interne ────────────────────────────────────────────────────────────────
162
+
163
+ private _assertTo(): void {
164
+ if (!this._to) throw new Error(`PathBuilder : .to(node) requis avant cette opération`)
165
+ }
166
+
167
+ private _findPaths(maxPaths: number, strategy?: Strategy): PathResult {
168
+ if (!this._to) throw new Error(`PathBuilder : .to(node) requis avant path()/paths()`)
169
+ const to = this._to
170
+
171
+ const penalty = S.toPenalty(strategy ?? this._opts.strategy ?? S.Shortest())
172
+ const finder = new PathFinder(this._graph)
173
+ const rawPaths = finder.findAllPaths(
174
+ this._from,
175
+ to,
176
+ maxPaths,
177
+ 50,
178
+ penalty,
179
+ this._opts.via,
180
+ this._opts.minHops ?? 0
181
+ )
182
+
183
+ if (rawPaths.length === 0) {
184
+ return { from: this._from, to, found: false, paths: [] }
185
+ }
186
+
187
+ const paths: ResolvedPath[] = rawPaths.map(nodes => ({
188
+ nodes,
189
+ steps: this._resolveSteps(nodes),
190
+ weight: this._computeWeight(nodes, penalty),
191
+ hops: nodes.length - 1,
192
+ }))
193
+
194
+ return { from: this._from, to, found: true, paths }
195
+ }
196
+
197
+ /**
198
+ * Enrichit les nodes avec labels et arêtes empruntées.
199
+ */
200
+ private _resolveSteps(nodes: string[]): PathStep[] {
201
+ const nodeMap = new Map(this._graph.nodes.map(n => [n.id, n]))
202
+
203
+ return nodes.map((nodeId, i) => {
204
+ const node = nodeMap.get(nodeId)
205
+ const step: PathStep = {
206
+ node: nodeId,
207
+ label: (node as any)?.label ?? (node as any)?.name ?? nodeId,
208
+ }
209
+ if (i > 0) {
210
+ // Arête qui mène à ce node depuis le précédent
211
+ step.via = this._graph.edges.find(
212
+ e => e.from === nodes[i - 1] && e.to === nodeId
213
+ ) ?? this._graph.edges.find(
214
+ e => e.from === nodeId && e.to === nodes[i - 1]
215
+ )
216
+ }
217
+ return step
218
+ })
219
+ }
220
+
221
+ /**
222
+ * Calcule le poids total d'un chemin en tenant compte de la pénalité
223
+ * de correspondance (changement de ligne/type d'arête).
224
+ */
225
+ private _computeWeight(nodes: string[], transferPenalty: number): number {
226
+ let weight = 0
227
+ let prevEdgeType: string | undefined
228
+
229
+ for (let i = 0; i < nodes.length - 1; i++) {
230
+ const edge = this._graph.edges.find(
231
+ e => e.from === nodes[i] && e.to === nodes[i + 1]
232
+ )
233
+ const w = edge ? Number(edge.weight) || 1 : 1
234
+ const type = edge?.metadata?.type ?? edge?.via
235
+
236
+ // Pénalité si changement de type d'arête (correspondance)
237
+ if (transferPenalty > 0 && prevEdgeType && type !== prevEdgeType) {
238
+ weight += transferPenalty
239
+ }
240
+
241
+ weight += w
242
+ prevEdgeType = type
243
+ }
244
+
245
+ return weight
246
+ }
247
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * api/index.ts — Re-exports du dossier api/
3
+ */
4
+ export { Graph } from './Graph.js'
5
+ export { PathBuilder } from './PathBuilder.js'
6
+ export {
7
+ Strategy,
8
+ } from './types.js'
9
+ export type {
10
+ PathResult,
11
+ ResolvedPath,
12
+ PathStep,
13
+ QueryResult,
14
+ PathBuilderOptions,
15
+ } from './types.js'