@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,152 @@
1
+ /**
2
+ * queries.ts — Requêtes métro Paris
3
+ *
4
+ * Remplace les config*.json éparpillés.
5
+ * Chaque requête a un nom, une description, et des paramètres typés.
6
+ *
7
+ * Usage :
8
+ * tsx cli/run-scenario.ts scenarios/test-metro-paris --query chatelet-opera
9
+ * tsx cli/run-scenario.ts scenarios/test-metro-paris --query all
10
+ */
11
+
12
+ import type { PathQuery } from '../../types/index.js'
13
+
14
+ export interface NamedQuery {
15
+ name: string
16
+ description: string
17
+ query: PathQuery
18
+ }
19
+
20
+ export const metroQueries: NamedQuery[] = [
21
+
22
+ // ============================================================
23
+ // TRAJETS SIMPLES — Une seule ligne, pas de correspondance
24
+ // ============================================================
25
+
26
+ {
27
+ name: 'chatelet-opera',
28
+ description: 'Châtelet → Opéra (Ligne 7, 2 stations, ~3 min)',
29
+ query: {
30
+ from: 'Station-chatelet',
31
+ to: 'Station-opera',
32
+ maxPaths: 3
33
+ }
34
+ },
35
+
36
+ {
37
+ name: 'ligne1-terminus',
38
+ description: 'La Défense → Château de Vincennes (Ligne 1 complète, ~45 min)',
39
+ query: {
40
+ from: 'Station-la-defense-grande-arche',
41
+ to: 'Station-chateau-de-vincennes',
42
+ maxPaths: 1
43
+ }
44
+ },
45
+
46
+ {
47
+ name: 'ligne4-nord-sud',
48
+ description: 'Porte de Clignancourt → Mairie de Montrouge (Ligne 4 complète)',
49
+ query: {
50
+ from: 'Station-porte-de-clignancourt',
51
+ to: 'Station-mairie-de-montrouge',
52
+ maxPaths: 1
53
+ }
54
+ },
55
+
56
+ // ============================================================
57
+ // TRAJETS AVEC CORRESPONDANCES — Teste le pathfinder
58
+ // ============================================================
59
+
60
+ {
61
+ name: 'republique-bastille',
62
+ description: 'République → Bastille (plusieurs chemins via L5, L8, L9)',
63
+ query: {
64
+ from: 'Station-republique',
65
+ to: 'Station-bastille',
66
+ maxPaths: 5
67
+ }
68
+ },
69
+
70
+ {
71
+ name: 'gare-du-nord-montparnasse',
72
+ description: 'Gare du Nord → Montparnasse (correspondance obligatoire)',
73
+ query: {
74
+ from: 'Station-gare-du-nord',
75
+ to: 'Station-montparnasse-bienvenue',
76
+ maxPaths: 3
77
+ }
78
+ },
79
+
80
+ {
81
+ name: 'defense-nation',
82
+ description: 'La Défense → Nation (traversée Est-Ouest, Ligne 1)',
83
+ query: {
84
+ from: 'Station-la-defense-grande-arche',
85
+ to: 'Station-nation',
86
+ maxPaths: 3
87
+ }
88
+ },
89
+
90
+ // ============================================================
91
+ // TRAJETS COMPLEXES — Hubs majeurs, longue distance
92
+ // ============================================================
93
+
94
+ {
95
+ name: 'saint-denis-chatillon',
96
+ description: 'Saint-Denis Université → Châtillon-Montrouge (Ligne 13 complète)',
97
+ query: {
98
+ from: 'Station-saint-denis-universite',
99
+ to: 'Station-chatillon-montrouge',
100
+ maxPaths: 2
101
+ }
102
+ },
103
+
104
+ {
105
+ name: 'vincennes-defense',
106
+ description: 'Château de Vincennes → La Défense (traversée complète Ligne 1)',
107
+ query: {
108
+ from: 'Station-chateau-de-vincennes',
109
+ to: 'Station-la-defense-grande-arche',
110
+ maxPaths: 1
111
+ }
112
+ },
113
+
114
+ {
115
+ name: 'clignancourt-vincennes',
116
+ description: 'Porte de Clignancourt → Château de Vincennes (diagonale NW→SE)',
117
+ query: {
118
+ from: 'Station-porte-de-clignancourt',
119
+ to: 'Station-chateau-de-vincennes',
120
+ maxPaths: 5
121
+ }
122
+ },
123
+
124
+ // ============================================================
125
+ // TOURISME — Stations emblématiques
126
+ // ============================================================
127
+
128
+ {
129
+ name: 'louvre-tour-eiffel',
130
+ description: 'Louvre-Rivoli → Trocadéro (musées)',
131
+ query: {
132
+ from: 'Station-louvre-rivoli',
133
+ to: 'Station-trocadero',
134
+ maxPaths: 3
135
+ }
136
+ },
137
+
138
+ {
139
+ name: 'notre-dame-sacre-coeur',
140
+ description: 'Cité → Abbesses (Notre-Dame → Sacré-Cœur)',
141
+ query: {
142
+ from: 'Station-cite',
143
+ to: 'Station-abbesses',
144
+ maxPaths: 3
145
+ }
146
+ }
147
+ ]
148
+
149
+ /**
150
+ * Requête par défaut (utilisée sans --query)
151
+ */
152
+ export const defaultQuery = metroQueries.find(q => q.name === 'chatelet-opera')!
@@ -0,0 +1,10 @@
1
+ {
2
+ "mode": "PATHFIND",
3
+ "description": "Trouver la chaîne de sampling entre deux artistes",
4
+ "pathQuery": {
5
+ "from": "Artist-Will-Smith",
6
+ "to": "Artist-Manu-Dibango",
7
+ "maxPaths": 3,
8
+ "maxHops": 8
9
+ }
10
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "nodes": [
3
+ { "id": "Artist-Manu-Dibango", "type": "entity" },
4
+ { "id": "Artist-Michael-Jackson", "type": "entity" },
5
+ { "id": "Artist-Will-Smith", "type": "entity" },
6
+ { "id": "Track-Soul-Makossa", "type": "entity" },
7
+ { "id": "Track-Wanna-Be-Startin", "type": "entity" },
8
+ { "id": "Track-Gettin-Jiggy", "type": "entity" }
9
+ ],
10
+ "edges": [
11
+ { "name": "created", "from": "Artist-Manu-Dibango", "to": "Track-Soul-Makossa", "via": "artist_tracks", "weight": 1 },
12
+ { "name": "created", "from": "Artist-Michael-Jackson", "to": "Track-Wanna-Be-Startin", "via": "artist_tracks", "weight": 1 },
13
+ { "name": "created", "from": "Artist-Will-Smith", "to": "Track-Gettin-Jiggy", "via": "artist_tracks", "weight": 1 },
14
+ { "name": "samples", "from": "Track-Wanna-Be-Startin", "to": "Track-Soul-Makossa", "via": "track_samples", "weight": 3 },
15
+ { "name": "samples", "from": "Track-Gettin-Jiggy", "to": "Track-Wanna-Be-Startin", "via": "track_samples", "weight": 3 },
16
+ { "name": "credited_on", "from": "Track-Soul-Makossa", "to": "Artist-Manu-Dibango", "via": "artist_tracks", "weight": 1 },
17
+ { "name": "credited_on", "from": "Track-Wanna-Be-Startin", "to": "Artist-Michael-Jackson", "via": "artist_tracks", "weight": 1 },
18
+ { "name": "credited_on", "from": "Track-Gettin-Jiggy", "to": "Artist-Will-Smith", "via": "artist_tracks", "weight": 1 }
19
+ ]
20
+ }
@@ -0,0 +1,23 @@
1
+ # Migration depuis l'ancien scénario Netflix
2
+
3
+ ## Fichiers supprimés
4
+
5
+ | Fichier | Raison |
6
+ |---------|--------|
7
+ | `actions.ts` | Mode SCHEDULE mis de côté — remplacé par `queries.ts` |
8
+ | `stack.json` | Pile pré-chargée pour SCHEDULE — plus pertinente |
9
+
10
+ ## Fichiers remplacés
11
+
12
+ | Ancien | Nouveau | Changement |
13
+ |--------|---------|------------|
14
+ | `config.json` mode `SCHEDULE` | `config.json` mode `NAVIGATE` | Abandon de SCHEDULE |
15
+ | `graph.json` conceptuel (Directors/Actors/Genres) | `graph.json` généré par pipeline | Remplacement complet |
16
+
17
+ ## Fichiers ajoutés
18
+
19
+ | Fichier | Description |
20
+ |---------|-------------|
21
+ | `data/` | 8 fichiers JSON + synonyms.json |
22
+ | `queries.ts` | 10 requêtes NAVIGATE/PATHFIND |
23
+ | `README.md` | Documentation complète |
@@ -0,0 +1,138 @@
1
+ # Scénario Netflix
2
+
3
+ Démonstration du **pipeline amont** de LinkLab sur des données JSON réelles.
4
+
5
+ Ce scénario est le seul qui illustre les deux côtés de LinkLab :
6
+
7
+ ```
8
+ Pipeline amont JsonSchemaExtractor → SchemaAnalyzer → GraphBuilder
9
+ → GraphAssembler → GraphOptimizer → graph.json
10
+
11
+ Pipeline aval PathFinder → NavigationEngine → résultats
12
+ ```
13
+
14
+ Le `graph.json` de ce scénario **n'est pas écrit à la main** — il est généré
15
+ automatiquement depuis les 8 fichiers JSON dans `data/`.
16
+
17
+ ---
18
+
19
+ ## Structure des données
20
+
21
+ ```
22
+ data/
23
+ categories.json 18 entrées id, name
24
+ companies.json vide
25
+ credits.json 2 957 entrées id, movieId, personId, jobId
26
+ departments.json 4 entrées id, name
27
+ jobs.json 28 entrées id, name, departmentId
28
+ movies.json 200 entrées id, title, categories[], releaseYear, ...
29
+ people.json 2 363 entrées id, gender, name
30
+ users.json 5 entrées id, name, isAdmin, preferences{}
31
+ synonyms.json synonymes projet (person→people, movie→movies, ...)
32
+ ```
33
+
34
+ ### Relations FK détectées automatiquement
35
+
36
+ | Colonne | Résolution | Stratégie |
37
+ |---------|------------|-----------|
38
+ | `credits.movieId` | → `movies.id` | pluriel régulier +s |
39
+ | `credits.personId` | → `people.id` | synonyme universel (person→people) |
40
+ | `credits.jobId` | → `jobs.id` | synonyme projet (job→jobs) |
41
+ | `jobs.departmentId` | → `departments.id` | correspondance directe |
42
+
43
+ ---
44
+
45
+ ## Graphe généré
46
+
47
+ ```
48
+ 7 nœuds movies, people, credits, jobs, departments, categories, users
49
+ 66 arêtes
50
+ physical × 4 FK déclarées
51
+ physical_reverse × 4 inverses automatiques
52
+ semantic_view × 56 movies ↔ people pour chacun des 28 jobs
53
+ virtual × 2 movies ↔ categories (array inline)
54
+ ```
55
+
56
+ ### Pourquoi 56 vues sémantiques ?
57
+
58
+ `credits` est un pivot entre `movies` et `people` avec un discriminant `jobId → jobs`.
59
+ Le `GraphBuilder` lit la table `jobs` (28 entrées) et génère automatiquement une
60
+ paire d'arêtes sémantiques par job :
61
+
62
+ ```
63
+ movies → people [actor] condition: { jobId: 1 }
64
+ people → movies [actor_in] condition: { jobId: 1 }
65
+ movies → people [director] condition: { jobId: 2 }
66
+ people → movies [director_in] condition: { jobId: 2 }
67
+ movies → people [writer] condition: { jobId: 3 }
68
+ ...
69
+ ```
70
+
71
+ Sans le pipeline, ces 56 relations devraient être écrites à la main.
72
+
73
+ ---
74
+
75
+ ## Lancer le scénario
76
+
77
+ ```bash
78
+ # Requête par défaut (directors-of-movie)
79
+ tsx cli/run-scenario.ts scenarios/test-netflix
80
+
81
+ # Requêtes disponibles
82
+ tsx cli/run-scenario.ts scenarios/test-netflix --query actors-of-movie
83
+ tsx cli/run-scenario.ts scenarios/test-netflix --query movies-of-director
84
+ tsx cli/run-scenario.ts scenarios/test-netflix --query movies-to-departments
85
+ tsx cli/run-scenario.ts scenarios/test-netflix --query people-to-movies-minhops
86
+ tsx cli/run-scenario.ts scenarios/test-netflix --query all
87
+ ```
88
+
89
+ ### Requêtes disponibles
90
+
91
+ | Nom | Description |
92
+ |-----|-------------|
93
+ | `directors-of-movie` | movies → people via vue `director` |
94
+ | `actors-of-movie` | movies → people via vue `actor` |
95
+ | `movies-of-director` | people → movies via vue `director_in` |
96
+ | `movies-of-actor` | people → movies via vue `actor_in` |
97
+ | `credits-to-jobs` | navigation physique credits → jobs |
98
+ | `jobs-to-departments` | navigation physique jobs → departments |
99
+ | `movies-to-categories` | relation virtuelle (array inline) |
100
+ | `movies-to-departments` | 2 sauts : movies → credits → jobs → departments |
101
+ | `people-to-departments` | 3 sauts via credits → jobs → departments |
102
+ | `people-to-movies-minhops` | chemin le plus court avec `minHops` |
103
+
104
+ ---
105
+
106
+ ## Régénérer le graphe
107
+
108
+ Si tu modifies les données, relance le pipeline pour régénérer `graph.json` :
109
+
110
+ ```bash
111
+ tsx cli/run-pipeline.ts scenarios/test-netflix/data --out scenarios/test-netflix/graph.json
112
+ ```
113
+
114
+ Le pipeline utilise `data/synonyms.json` pour résoudre les FK irrégulières
115
+ (`personId → people`, `jobId → jobs`) en complément de `config/synonyms.json`
116
+ (synonymes universels).
117
+
118
+ ---
119
+
120
+ ## Ce que ce scénario ne couvre pas
121
+
122
+ | Donnée | Raison |
123
+ |--------|--------|
124
+ | `movies.categories` (array inline) | Relation virtuelle générée, mais sans garantie de correspondance exacte des ids |
125
+ | `users.preferences` (objet imbriqué) | Pas de FK — structure non relationnelle |
126
+ | `people/` et `movies/` (sous-dossiers détail) | Fichiers enrichis pour l'application — hors scope du pipeline |
127
+ | `companies.json` (vide) | Ignoré automatiquement |
128
+
129
+ ---
130
+
131
+ ## Comparaison avec les autres scénarios
132
+
133
+ | Scénario | Source | Ce qu'il démontre |
134
+ |----------|--------|-------------------|
135
+ | **Netflix** | JSON → pipeline | pipeline amont + vues sémantiques auto-générées |
136
+ | Musiciens | graph manuel | PATHFIND, `via`, `minHops`, cycles |
137
+ | Métro Paris | graph manuel | Dijkstra, poids, transferPenalty |
138
+ | DVDRental | PostgreSQL → pipeline | FK implicites, SchemaExtractor SQL |
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Actions du scénario Netflix
3
+ *
4
+ * Simule le parcours d'un utilisateur qui :
5
+ * 1. Voit la liste des films d'un réalisateur (déjà en contexte)
6
+ * 2. Sélectionne un film
7
+ * 3. Navigue vers les acteurs
8
+ * 4. Sélectionne un acteur
9
+ * 5. Explore les options à partir de cet acteur
10
+ */
11
+
12
+ import type { ScheduleAction, Frame } from '../../types/index.js'
13
+
14
+ const actions: ScheduleAction[] = [
15
+ {
16
+ name: 'selectMovie',
17
+ weight: 10,
18
+ // Movies est résolu (liste chargée) mais pas encore d'ID choisi
19
+ when: (stack: Frame[]) => {
20
+ const movies = stack.find(f => f.entity === 'Movies')
21
+ return movies?.state === 'RESOLVED' && movies.id == null
22
+ },
23
+ execute: async (stack: Frame[]) => {
24
+ console.log(' 🎬 [selectMovie] L\'utilisateur choisit un film dans la liste...')
25
+ const movies = stack.find(f => f.entity === 'Movies')
26
+ if (movies) {
27
+ movies.id = 10
28
+ console.log(' ✓ Film #10 sélectionné')
29
+ }
30
+ return stack
31
+ },
32
+ cooldown: 0
33
+ },
34
+
35
+ {
36
+ name: 'navigateToActors',
37
+ weight: 8,
38
+ // Un film est sélectionné, mais pas encore de frame Actors
39
+ when: (stack: Frame[]) => {
40
+ const movies = stack.find(f => f.entity === 'Movies')
41
+ const actors = stack.find(f => f.entity === 'Actors')
42
+ return !!movies && movies.id != null && !actors
43
+ },
44
+ execute: async (stack: Frame[]) => {
45
+ console.log(' 🎭 [navigateToActors] L\'utilisateur clique sur "Voir les acteurs"...')
46
+ stack.push({ entity: 'Actors', state: 'UNRESOLVED' })
47
+ console.log(' ✓ Frame Actors ajoutée à la stack')
48
+ return stack
49
+ },
50
+ cooldown: 0
51
+ },
52
+
53
+ {
54
+ name: 'selectActor',
55
+ weight: 5,
56
+ // Actors existe, est résolu, mais pas encore d'ID choisi
57
+ when: (stack: Frame[]) => {
58
+ const actors = stack.find(f => f.entity === 'Actors')
59
+ return !!actors && actors.state === 'RESOLVED' && actors.id == null
60
+ },
61
+ execute: async (stack: Frame[]) => {
62
+ console.log(' ⭐ [selectActor] L\'utilisateur sélectionne un acteur...')
63
+ const actors = stack.find(f => f.entity === 'Actors')
64
+ if (actors) {
65
+ actors.id = 3
66
+ console.log(' ✓ Acteur #3 sélectionné')
67
+ }
68
+ return stack
69
+ },
70
+ cooldown: 0
71
+ },
72
+
73
+ {
74
+ name: 'exploreFromActor',
75
+ weight: 3,
76
+ terminal: true, // S'exécute une seule fois
77
+ when: (stack: Frame[]) => {
78
+ const actors = stack.find(f => f.entity === 'Actors')
79
+ return !!actors && actors.id != null
80
+ },
81
+ execute: async (stack: Frame[]) => {
82
+ console.log(' 🔍 [exploreFromActor] Affichage des options d\'exploration :')
83
+ console.log(' - Filmographie complète')
84
+ console.log(' - Autres réalisateurs')
85
+ console.log(' - Co-stars')
86
+ return stack
87
+ },
88
+ cooldown: 0
89
+ }
90
+ ]
91
+
92
+ export default actions
@@ -0,0 +1,6 @@
1
+ {
2
+ "mode": "NAVIGATE",
3
+ "description": "Scénario Netflix — navigation dans un graphe généré automatiquement par le pipeline. Démontre les vues sémantiques (actor, director, writer...) issues de credits.jobId → jobs.",
4
+ "dataPath": "./data",
5
+ "graphGeneratedBy": "JsonSchemaExtractor → SchemaAnalyzer → GraphBuilder → GraphAssembler → GraphOptimizer"
6
+ }
@@ -0,0 +1 @@
1
+ [{"id":18,"name":"Drame"},{"id":80,"name":"Crime"},{"id":36,"name":"Histoire"},{"id":10752,"name":"Guerre"},{"id":35,"name":"Comédie"},{"id":10749,"name":"Romance"},{"id":16,"name":"Animation"},{"id":10751,"name":"Familial"},{"id":14,"name":"Fantastique"},{"id":28,"name":"Action"},{"id":53,"name":"Thriller"},{"id":12,"name":"Aventure"},{"id":37,"name":"Western"},{"id":10402,"name":"Musique"},{"id":27,"name":"Horreur"},{"id":9648,"name":"Mystère"},{"id":878,"name":"Science-Fiction"},{"id":10770,"name":"Téléfilm"}]