@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,207 @@
1
+ /**
2
+ * loadGraph.ts — Factory universelle pour charger un graphe LinkLab
3
+ *
4
+ * Point d'entrée recommandé pour 80% des cas.
5
+ * Déduit le mode de chargement depuis le paramètre source et l'environnement.
6
+ *
7
+ * Usage :
8
+ * // Node — fichier local
9
+ * const cinema = await loadGraph('./cinema.json', { provider })
10
+ *
11
+ * // Browser ou Node — URL HTTP
12
+ * const cinema = await loadGraph('https://cdn.example.com/cinema.json', { provider })
13
+ *
14
+ * // Données déjà en mémoire (tests, browser, injection)
15
+ * const cinema = await loadGraph({ compiled }, { provider })
16
+ *
17
+ * Convention fichiers (Node) :
18
+ * cinema.json ← graphe compilé (requis)
19
+ * cinema.reference.json ← graphe brut (optionnel — chargé automatiquement)
20
+ * cinema.override.json ← surcharges dev (optionnel — chargé automatiquement)
21
+ *
22
+ * new Graph() reste disponible pour les niveaux 2/3 et les tests unitaires.
23
+ */
24
+
25
+ import { Graph } from './Graph.js'
26
+ import type { GraphOptions } from './Graph.js'
27
+ import type { Graph as GraphData, CompiledGraph } from '../types/index.js'
28
+
29
+ // ── Types ─────────────────────────────────────────────────────────────────────
30
+
31
+ /** Source du graphe compilé */
32
+ export type GraphSource =
33
+ | string // chemin fichier (Node) ou URL http(s) (browser/Node)
34
+ | GraphSourceObject
35
+
36
+ export interface GraphSourceObject {
37
+ compiled: CompiledGraph
38
+ reference?: GraphData
39
+ overrides?: Record<string, any>
40
+ }
41
+
42
+ /** Options de loadGraph — étend GraphOptions */
43
+ export interface LoadGraphOptions extends Omit<GraphOptions, 'compiled'> {
44
+ /** Surcharge le chemin du graphe de référence (optionnel) */
45
+ reference?: string
46
+ /** Surcharge le chemin des overrides (optionnel) */
47
+ overrides?: string
48
+ /** Dossier contenant les fichiers JSON de données {entity}.json (mode local) */
49
+ dataDir?: string
50
+ }
51
+
52
+ // ── Détection environnement ────────────────────────────────────────────────────
53
+
54
+ const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node
55
+ const IS_BROWSER = typeof (globalThis as any).window !== 'undefined'
56
+
57
+ // ── Chargement JSON ────────────────────────────────────────────────────────────
58
+
59
+ async function loadJSON(source: string): Promise<any> {
60
+ // URL HTTP/HTTPS → fetch (universel)
61
+ if (source.startsWith('http://') || source.startsWith('https://')) {
62
+ const res = await fetch(source)
63
+ if (!res.ok) throw new Error(`loadGraph: HTTP ${res.status} — ${source}`)
64
+ return res.json()
65
+ }
66
+
67
+ // Chemin fichier → Node uniquement
68
+ if (IS_NODE) {
69
+ const { createRequire } = await import('module')
70
+ const { fileURLToPath } = await import('url')
71
+ const pathModule = await import('path')
72
+ const fsModule = await import('fs')
73
+
74
+ // Résoudre le chemin depuis cwd
75
+ const resolved = pathModule.default.resolve(source)
76
+ if (!fsModule.default.existsSync(resolved)) return null
77
+
78
+ // createRequire depuis cwd pour les JSON
79
+ const req = createRequire(pathModule.default.join(process.cwd(), 'noop.js'))
80
+ return req(resolved)
81
+ }
82
+
83
+ throw new Error(`loadGraph: chemin fichier non supporté dans ce contexte — utiliser une URL HTTP`)
84
+ }
85
+
86
+ /** Derive les chemins convention depuis un chemin de base */
87
+ function deriveConventionPaths(basePath: string): {
88
+ compiled: string
89
+ reference: string
90
+ overrides: string
91
+ dictionary: string
92
+ } {
93
+ // './cinema.json' → base = './cinema'
94
+ // './cinema' → base = './cinema'
95
+ const base = basePath.replace(/\.json$/i, '')
96
+ return {
97
+ compiled: `${base}.json`,
98
+ reference: `${base}.reference.gen.json`,
99
+ overrides: `${base}.override.json`,
100
+ dictionary: `${base}.dictionary.gen.json`,
101
+ }
102
+ }
103
+
104
+ // ── Factory principale ────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * loadGraph — charge un graphe LinkLab depuis n'importe quelle source.
108
+ *
109
+ * Retourne directement le proxy sémantique (niveau 1) — prêt à naviguer.
110
+ * Pour accéder au Graph sous-jacent (niveaux 2/3/4) : domain.graph
111
+ *
112
+ * @example
113
+ * const cinema = await loadGraph('./cinema.json', { provider })
114
+ * await cinema.directors('Nolan').movies
115
+ * cinema.graph.from('movies').to('people').path() // niveau 2
116
+ */
117
+ export async function loadGraph(
118
+ source: GraphSource,
119
+ options: LoadGraphOptions = {}
120
+ ): Promise<ReturnType<Graph['domain']>> {
121
+
122
+ let compiled: CompiledGraph
123
+ let reference: GraphData | null = null
124
+ let overrides: Record<string, any> | null = null
125
+ let dictionary: Record<string, any> | null = null
126
+
127
+ // ── Source objet en mémoire ────────────────────────────────────────────────
128
+ if (typeof source === 'object') {
129
+ compiled = source.compiled
130
+ reference = source.reference ?? null
131
+ overrides = source.overrides ?? null
132
+ }
133
+
134
+ // ── Source string (chemin ou URL) ─────────────────────────────────────────
135
+ else {
136
+ const isURL = source.startsWith('http://') || source.startsWith('https://')
137
+
138
+ if (isURL) {
139
+ // URL directe — pas de convention (on ne peut pas deviner les URLs sœurs)
140
+ compiled = await loadJSON(source)
141
+ if (!compiled) throw new Error(`loadGraph: impossible de charger ${source}`)
142
+ } else {
143
+ // Chemin fichier — convention {name}.json + {name}.reference.json + {name}.override.json
144
+ const paths = deriveConventionPaths(source)
145
+
146
+ compiled = await loadJSON(options.reference ? source : paths.compiled)
147
+ if (!compiled) throw new Error(`loadGraph: graphe introuvable — ${paths.compiled}`)
148
+
149
+ // Référence (optionnel — chargé silencieusement)
150
+ const refPath = options.reference ?? paths.reference
151
+ reference = await loadJSON(refPath) // null si absent
152
+
153
+ // Overrides (optionnel — chargé silencieusement)
154
+ const ovPath = options.overrides ?? paths.overrides
155
+ overrides = await loadJSON(ovPath) // null si absent
156
+
157
+ // Dictionnaire résolu (optionnel — labels humains des routes)
158
+ dictionary = await loadJSON(paths.dictionary) // null si absent
159
+ }
160
+ }
161
+
162
+ // ── Charger dataset depuis dataDir ────────────────────────────────────────
163
+ let dataset = options.dataset ?? null
164
+
165
+ if (!dataset && options.dataDir && IS_NODE) {
166
+ const pathModule = await import('path')
167
+ const fsModule = await import('fs')
168
+ const { createRequire } = await import('module')
169
+ const req = createRequire(pathModule.default.join(process.cwd(), 'noop.js'))
170
+ const dataDirAbs = pathModule.default.resolve(options.dataDir)
171
+ dataset = {}
172
+ for (const node of compiled.nodes) {
173
+ const file = pathModule.default.join(dataDirAbs, `${node.id}.json`)
174
+ if (fsModule.default.existsSync(file)) dataset[node.id] = req(file)
175
+ }
176
+ }
177
+
178
+ // ── Appliquer les overrides sur le compilé ─────────────────────────────────
179
+ // TODO : deepMerge(compiled, overrides) quand ADR-0008 sera implémenté
180
+ if (overrides && IS_NODE && process.env.LINKLAB_DEBUG) {
181
+ console.warn('[loadGraph] overrides chargés mais pas encore appliqués (ADR-0008 pending)')
182
+ }
183
+
184
+ // ── Construire le Graph (niveau bas) ──────────────────────────────────────
185
+ const rawGraph: GraphData = reference ?? {
186
+ nodes: compiled.nodes,
187
+ edges: [],
188
+ }
189
+
190
+ const { compiled: _c, reference: _r, overrides: _o, dataDir: _d, dataset: _ds, ...graphOptions } = options as any
191
+ const graph = new Graph(rawGraph, {
192
+ ...graphOptions,
193
+ compiled,
194
+ ...(dataset ? { dataset } : {}),
195
+ ...(dictionary ? { dictionary } : {}),
196
+ })
197
+
198
+ // ── Retourner le domain directement ───────────────────────────────────────
199
+ // loadGraph() retourne le proxy sémantique (niveau 1) — pas le Graph brut.
200
+ // Accès au Graph sous-jacent via : const g = domain.graph
201
+ return graph.domain()
202
+ }
203
+
204
+ // ── Export de commodité ────────────────────────────────────────────────────────
205
+
206
+ /** Alias — même API, nom plus court pour les imports fréquents */
207
+ export { loadGraph as graph }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * test-api.ts — Validation niveau 2 sur les 3 exemples
3
+ *
4
+ * Couvre les deux familles :
5
+ * A) Pathfinding pur — metro, musicians (pas de données)
6
+ * B) Navigation data — netflix (compiled + dataset)
7
+ */
8
+
9
+ import { createRequire } from 'module'
10
+ import { fileURLToPath } from 'url'
11
+ import path from 'path'
12
+ import { Graph, Strategy } from './index.js'
13
+
14
+ const require = createRequire(import.meta.url)
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
+ const root = path.join(__dirname, '..')
17
+
18
+ // ── Helpers ──────────────────────────────────────────────────────────────────
19
+
20
+ const ok = (label: string) => console.log(` ✅ ${label}`)
21
+ const err = (label: string, e: any) => console.log(` ❌ ${label}: ${e?.message ?? e}`)
22
+ const sep = (title: string) => console.log(`\n${'─'.repeat(50)}\n${title}`)
23
+
24
+ // ── Famille A : Pathfinding pur ───────────────────────────────────────────────
25
+
26
+ sep('METRO — Pathfinding pur')
27
+ try {
28
+ const metro = new Graph(require(`${root}/examples/metro/graph.json`))
29
+
30
+ // Chemin optimal (Shortest)
31
+ const r1 = metro.from('Station-chatelet').to('Station-opera').path()
32
+ r1.found && r1.paths[0].hops > 0
33
+ ? ok(`Châtelet→Opéra : ${r1.paths[0].hops} saut(s), poids=${r1.paths[0].weight}`)
34
+ : err('Châtelet→Opéra', 'chemin non trouvé')
35
+
36
+ // Mode confort — pénalise les correspondances
37
+ const r2 = metro.from('Station-republique').to('Station-bastille')
38
+ .paths(Strategy.Comfort())
39
+ r2.found
40
+ ? ok(`République→Bastille Comfort : ${r2.paths.length} chemins`)
41
+ : err('République→Bastille', 'non trouvé')
42
+
43
+ // Mode LeastHops
44
+ const r3 = metro.from('Station-gare-du-nord').to('Station-montparnasse-bienvenue')
45
+ .path(Strategy.LeastHops())
46
+ r3.found
47
+ ? ok(`GdN→Montparnasse LeastHops : ${r3.paths[0].hops} sauts`)
48
+ : err('GdN→Montparnasse', 'non trouvé')
49
+
50
+ // Introspection
51
+ console.log(`\n graph.entities : ${metro.entities.length} nodes`)
52
+ console.log(` graph.relations : ${metro.relations.length} arêtes`)
53
+ const types = Object.keys(metro.schema)
54
+ console.log(` graph.schema : types = [${types.join(', ')}]`)
55
+
56
+ } catch(e) { err('Metro init', e) }
57
+
58
+ sep('MUSICIANS — Pathfinding avec via + minHops')
59
+ try {
60
+ const music = new Graph(require(`${root}/examples/musicians/graph.json`))
61
+
62
+ // Chaîne sampling Will Smith → Manu Dibango
63
+ const r1 = music.from('artist-will-smith').to('artist-manu-dibango')
64
+ .paths()
65
+ r1.found
66
+ ? ok(`Will Smith→Manu Dibango : ${r1.paths.length} chemin(s), meilleur=${r1.paths[0].hops} sauts`)
67
+ : err('Will Smith→Manu Dibango', 'non trouvé')
68
+
69
+ // Chemin d'influence James Brown → Kanye avec minHops
70
+ const builder = music.from('artist-james-brown', { minHops: 1 })
71
+ .to('artist-kanye-west')
72
+ const r2 = builder.paths()
73
+ r2.found
74
+ ? ok(`James Brown→Kanye (minHops=1) : ${r2.paths.length} chemin(s)`)
75
+ : err('James Brown→Kanye', 'non trouvé')
76
+
77
+ // .links — vue structurelle
78
+ const l = music.from('artist-daft-punk').to('artist-kanye-west').links
79
+ l.found
80
+ ? ok(`.links Daft Punk↔Kanye : ${l.edges.length} arêtes dans le sous-graphe`)
81
+ : err('.links', 'non trouvé')
82
+
83
+ // Steps enrichis — labels lisibles
84
+ if (r1.found && r1.paths[0].steps.length > 0) {
85
+ const labels = r1.paths[0].steps.map(s => s.label ?? s.node).join(' → ')
86
+ ok(`Steps : ${labels}`)
87
+ }
88
+
89
+ } catch(e) { err('Musicians init', e) }
90
+
91
+ sep('NETFLIX — Navigation avec données')
92
+ try {
93
+ const compiled = require(`${root}/examples/netflix/compiled-graph.json`)
94
+ const movies = require(`${root}/scenarios/test-netflix/data/movies.json`)
95
+ const credits = require(`${root}/scenarios/test-netflix/data/credits.json`)
96
+ const people = require(`${root}/scenarios/test-netflix/data/people.json`)
97
+
98
+ const netflix = new Graph(
99
+ require(`${root}/scenarios/test-netflix/graph.json`),
100
+ { compiled, dataset: { movies, credits, people } }
101
+ )
102
+
103
+ // Traversée movies → people via execute()
104
+ const r1 = await netflix.from('movies').to('people').execute({ id: 278 })
105
+ r1.data.length > 0
106
+ ? ok(`movies(278)→people : ${r1.data.length} personnes en ${r1.timing}ms, path=${r1.path.join('→')}`)
107
+ : err('movies→people', 'data vide')
108
+
109
+ // Traversée people → movies
110
+ const r2 = await netflix.from('people').to('movies').execute({ id: 4027 })
111
+ r2.data.length > 0
112
+ ? ok(`people(4027)→movies : ${r2.data.length} films (${(r2.data as any[]).map((m:any)=>m.title).join(', ')})`)
113
+ : err('people→movies', 'data vide')
114
+
115
+ // path() fonctionne aussi (sans données)
116
+ const r3 = netflix.from('movies').to('people').path()
117
+ r3.found
118
+ ? ok(`path() movies→people : ${r3.paths[0].nodes.join('→')}`)
119
+ : err('path() movies→people', 'non trouvé')
120
+
121
+ // Introspection
122
+ console.log(`\n graph.entities : ${netflix.entities.length} nodes`)
123
+ console.log(` graph.weights : ${Object.keys(netflix.weights).length} arêtes pondérées`)
124
+
125
+ } catch(e) { err('Netflix init', e) }
126
+
127
+ sep('NIVEAU 4 — Maintenance')
128
+ try {
129
+ const music = new Graph(require(`${root}/examples/musicians/graph.json`))
130
+
131
+ // weight().set()
132
+ const edge = music.relations.find(e => e.name)!
133
+ const g2 = music.weight(edge.name!).set(99)
134
+ const before = music.weights[edge.name!]
135
+ const after = g2.weights[edge.name!]
136
+ before !== after && after === 99
137
+ ? ok(`weight('${edge.name}').set(99) : ${before} → ${after}`)
138
+ : err('weight.set', `${before} → ${after}`)
139
+
140
+ // Immuabilité — le graph original n'est pas modifié
141
+ music.weights[edge.name!] === before
142
+ ? ok(`Immuabilité préservée : original = ${before}`)
143
+ : err('Immuabilité', 'graph original modifié')
144
+
145
+ // snapshot()
146
+ const snap = music.snapshot()
147
+ snap.graph && !snap.compiled
148
+ ? ok(`snapshot() : graph OK, compiled=null (pas de compile())`)
149
+ : err('snapshot', JSON.stringify(snap))
150
+
151
+ } catch(e) { err('Niveau 4', e) }
152
+
153
+ console.log('\n' + '─'.repeat(50))
@@ -0,0 +1,119 @@
1
+ /**
2
+ * test-domain.ts — Validation niveau 1 : DomainProxy
3
+ *
4
+ * Couvre :
5
+ * - Accès propriété simple cinema.movies
6
+ * - Filtre par ID (number) cinema.people(278)
7
+ * - Filtre par objet cinema.people({ id: 278 })
8
+ * - Traversée thenable await cinema.people(278).movies
9
+ * - Fetch direct await cinema.movies
10
+ * - Chaînage profond await cinema.people(278).movies (depth 2)
11
+ */
12
+
13
+ import { createRequire } from 'module'
14
+ import { fileURLToPath } from 'url'
15
+ import path from 'path'
16
+ import { Graph } from './index.js'
17
+
18
+ const require = createRequire(import.meta.url)
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
20
+ const root = path.join(__dirname, '..')
21
+
22
+ const ok = (label: string) => console.log(` ✅ ${label}`)
23
+ const err = (label: string, detail: any) => console.log(` ❌ ${label}: ${detail?.message ?? JSON.stringify(detail)}`)
24
+ const sep = (t: string) => console.log(`\n${'─'.repeat(50)}\n${t}`)
25
+
26
+ // ── Setup Netflix ─────────────────────────────────────────────────────────────
27
+
28
+ const compiled = require(`${root}/examples/netflix/compiled-graph.json`)
29
+ const movies = require(`${root}/scenarios/test-netflix/data/movies.json`)
30
+ const credits = require(`${root}/scenarios/test-netflix/data/credits.json`)
31
+ const people = require(`${root}/scenarios/test-netflix/data/people.json`)
32
+
33
+ const cinema = new Graph(
34
+ require(`${root}/scenarios/test-netflix/graph.json`),
35
+ { compiled, dataset: { movies, credits, people } }
36
+ ).domain()
37
+
38
+ sep('NIVEAU 1 — Fetch direct (depth 1)')
39
+ try {
40
+ // await cinema.movies → tous les films
41
+ const r1 = await (cinema as any).movies
42
+ r1.data.length > 0
43
+ ? ok(`await cinema.movies → ${r1.data.length} films`)
44
+ : err('cinema.movies', 'vide')
45
+
46
+ // await cinema.people → toutes les personnes
47
+ const r2 = await (cinema as any).people
48
+ r2.data.length > 0
49
+ ? ok(`await cinema.people → ${r2.data.length} personnes`)
50
+ : err('cinema.people', 'vide')
51
+
52
+ // await cinema.movies(278) → un seul film
53
+ const r3 = await (cinema as any).movies(278)
54
+ r3.data.length === 1 && r3.data[0].title === 'Les Évadés'
55
+ ? ok(`await cinema.movies(278) → "${r3.data[0].title}"`)
56
+ : err('cinema.movies(278)', r3.data)
57
+
58
+ // await cinema.people(4027) → Frank Darabont
59
+ const r4 = await (cinema as any).people(4027)
60
+ r4.data.length === 1 && r4.data[0].name === 'Frank Darabont'
61
+ ? ok(`await cinema.people(4027) → "${r4.data[0].name}"`)
62
+ : err('cinema.people(4027)', r4.data)
63
+
64
+ // Filtre par objet
65
+ const r5 = await (cinema as any).people({ id: 4027 })
66
+ r5.data.length === 1
67
+ ? ok(`await cinema.people({ id: 4027 }) → "${r5.data[0].name}"`)
68
+ : err('cinema.people({id:4027})', r5.data)
69
+
70
+ } catch(e) { err('Depth 1', e) }
71
+
72
+ sep('NIVEAU 1 — Traversée (depth 2)')
73
+ try {
74
+ // await cinema.people(4027).movies → filmographie Darabont
75
+ const r1 = await (cinema as any).people(4027).movies
76
+ r1.data.length >= 2
77
+ ? ok(`await cinema.people(4027).movies → ${r1.data.length} films : ${r1.data.map((m:any)=>m.title).join(', ')}`)
78
+ : err('cinema.people(4027).movies', `${r1.data.length} films`)
79
+
80
+ // await cinema.movies(278).people → cast des Évadés
81
+ const r2 = await (cinema as any).movies(278).people
82
+ r2.data.length >= 10
83
+ ? ok(`await cinema.movies(278).people → ${r2.data.length} personnes`)
84
+ : err('cinema.movies(278).people', `${r2.data.length}`)
85
+
86
+ // Trail path
87
+ r1.path.length > 1
88
+ ? ok(`path: ${r1.path.join('→')}`)
89
+ : err('path', r1.path)
90
+
91
+ } catch(e) { err('Depth 2', e) }
92
+
93
+ sep('NIVEAU 1 — Clé sémantique (string filter)')
94
+ try {
95
+ // await cinema.movies('Les Évadés') → via title
96
+ // Le semantic_key pour movies devrait être 'title'
97
+ const r1 = await (cinema as any).movies({ title: 'Les Évadés' })
98
+ r1.data.length === 1
99
+ ? ok(`await cinema.movies({ title: 'Les Évadés' }) → id=${r1.data[0].id}`)
100
+ : err('cinema.movies({title})', `${r1.data.length} résultats`)
101
+
102
+ } catch(e) { err('Semantic key', e) }
103
+
104
+ sep('NIVEAU 1 — Musicians (nodes par type)')
105
+ try {
106
+ const music = new Graph(
107
+ require(`${root}/examples/musicians/graph.json`)
108
+ ).domain()
109
+
110
+ // Les musicians ont type='artist' — pas de données, juste des nodes
111
+ // cinema.artists → devrait résoudre le type 'artist'
112
+ const node = (music as any).artists
113
+ node !== undefined
114
+ ? ok(`cinema.artists → DomainNode résolu (entity=${node.entity})`)
115
+ : err('cinema.artists', 'undefined')
116
+
117
+ } catch(e) { err('Musicians domain', e) }
118
+
119
+ console.log('\n' + '─'.repeat(50))
@@ -0,0 +1,88 @@
1
+ /**
2
+ * api/types.ts — Types publics de l'API LinkLab niveau 2+
3
+ *
4
+ * Ces types sont la surface visible pour les utilisateurs du moteur.
5
+ * Les types internes (CompiledGraph, RouteInfo, etc.) restent dans types/index.ts.
6
+ */
7
+
8
+ import type { GraphEdge } from '../types/index.js'
9
+
10
+ // ── Stratégies de pathfinding ────────────────────────────────────────────────
11
+
12
+ /**
13
+ * Strategy — comment Dijkstra pondère les chemins.
14
+ *
15
+ * Shortest : poids brut des arêtes — temps pur, distance minimale
16
+ * Comfort : pénalité par correspondance (+8 unités) — moins de changements
17
+ * LeastHops : favorise les chemins avec peu d'étapes
18
+ * Custom(n) : pénalité explicite par correspondance
19
+ */
20
+ export type Strategy =
21
+ | { type: 'Shortest' }
22
+ | { type: 'Comfort' }
23
+ | { type: 'LeastHops' }
24
+ | { type: 'Custom'; transferPenalty: number }
25
+
26
+ // Factories — évitent les objets littéraux à l'usage
27
+ export const Strategy = {
28
+ Shortest: (): Strategy => ({ type: 'Shortest' }),
29
+ Comfort: (): Strategy => ({ type: 'Comfort' }),
30
+ LeastHops: (): Strategy => ({ type: 'LeastHops' }),
31
+ Custom: (transferPenalty: number): Strategy => ({ type: 'Custom', transferPenalty }),
32
+
33
+ toPenalty(s: Strategy): number {
34
+ switch (s.type) {
35
+ case 'Shortest': return 0
36
+ case 'Comfort': return 8
37
+ case 'LeastHops': return 50
38
+ case 'Custom': return s.transferPenalty
39
+ }
40
+ }
41
+ } as const
42
+
43
+ // ── Résultats de pathfinding ─────────────────────────────────────────────────
44
+
45
+ export interface PathStep {
46
+ node: string // ID du node
47
+ label?: string // label lisible si présent dans le graph
48
+ via?: GraphEdge // arête empruntée pour arriver ici (absente pour le premier node)
49
+ }
50
+
51
+ export interface ResolvedPath {
52
+ nodes: string[] // séquence d'IDs : ['Pigalle', 'Liège', 'Europe', ...]
53
+ steps: PathStep[] // version enrichie avec labels et arêtes
54
+ weight: number // poids total selon la stratégie appliquée
55
+ hops: number // nombre d'arêtes traversées
56
+ }
57
+
58
+ export interface PathResult {
59
+ from: string
60
+ to: string
61
+ found: boolean
62
+ paths: ResolvedPath[]
63
+ }
64
+
65
+ // ── Résultats de navigation avec données ─────────────────────────────────────
66
+
67
+ /**
68
+ * QueryResult — retourné par PathBuilder.execute()
69
+ * Uniquement en mode données (netflix, dvdrental) — pas pour metro/musicians.
70
+ */
71
+ export interface QueryResult<T = Record<string, any>> {
72
+ from: string
73
+ to: string
74
+ filters: Record<string, any>
75
+ data: T[]
76
+ path: string[]
77
+ timing: number
78
+ }
79
+
80
+ // ── Options du PathBuilder ───────────────────────────────────────────────────
81
+
82
+ export interface PathBuilderOptions {
83
+ maxPaths?: number
84
+ minHops?: number
85
+ maxHops?: number
86
+ via?: string[]
87
+ strategy?: Strategy
88
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "_comment": "Singular → plural irregular mappings for FK resolution. Used by JsonSchemaExtractor to resolve *Id columns to their target table. Add project-specific synonyms in examples/<project>/synonyms.json.",
3
+
4
+ "person": "people",
5
+ "man": "men",
6
+ "woman": "women",
7
+ "child": "children",
8
+
9
+ "company": "companies",
10
+ "category": "categories",
11
+ "country": "countries",
12
+ "city": "cities",
13
+ "currency": "currencies",
14
+ "industry": "industries",
15
+ "activity": "activities",
16
+ "facility": "facilities",
17
+ "capability": "capabilities",
18
+ "authority": "authorities",
19
+ "community": "communities",
20
+
21
+ "datum": "data",
22
+ "medium": "media",
23
+ "criterion": "criteria",
24
+ "phenomenon": "phenomena",
25
+ "analysis": "analyses",
26
+ "basis": "bases",
27
+ "thesis": "theses"
28
+ }