@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
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ # @linklabjs/core
2
+
3
+ > **The graph is the map. The Trail is the traveler.**
4
+
5
+ > "The Trail defines the path, the history, and the intention.
6
+ > The graph knows the possibilities."
7
+
8
+ LinkLab associates two concepts:
9
+
10
+ - **The compiled graph** — the map: entities, relations, optimal routes
11
+ - **The Trail** — the traveler: navigation, context, history, intention
12
+
13
+ The map knows all paths. The traveler decides where to go — and by traveling, enriches the map.
14
+
15
+ ---
16
+
17
+ ## The problem LinkLab solves
18
+
19
+ In every application, we write the same SQL joins by hand:
20
+
21
+ ```sql
22
+ -- Get all actors in films directed by Nolan
23
+ SELECT people.*
24
+ FROM directors
25
+ INNER JOIN credits ON directors.id = credits.personId AND credits.jobId = 2
26
+ INNER JOIN movies ON credits.movieId = movies.id
27
+ INNER JOIN credits c2 ON movies.id = c2.movieId AND c2.jobId = 1
28
+ INNER JOIN people ON c2.personId = people.id
29
+ WHERE directors.name = 'Nolan'
30
+ ```
31
+
32
+ With LinkLab:
33
+
34
+ ```typescript
35
+ cinema.directors('Nolan').movies.actors
36
+ ```
37
+
38
+ LinkLab generates the SQL, finds the optimal path in the graph, and improves continuously from usage traces.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install @linklabjs/core
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Quick start
51
+
52
+ ```typescript
53
+ import { Graph } from '@linklabjs/core'
54
+ import compiledGraph from './linklab/netflix/netflix.json'
55
+ import * as dataset from './data'
56
+
57
+ const graph = new Graph(compiledGraph, { dataset })
58
+ const netflix = graph.domain()
59
+
60
+ // Fluent navigation
61
+ const actors = await netflix.movies(278).actors
62
+ const films = await netflix.directors('Nolan').movies
63
+ const colleagues = await netflix.actors('DiCaprio').movies.directors
64
+ ```
65
+
66
+ The result is a plain JavaScript array — map, filter, sort as usual:
67
+
68
+ ```typescript
69
+ const titles = await netflix.directors('Nolan').movies
70
+ .then(films => films.filter(f => f.release_year > 2000))
71
+ .then(films => films.map(f => f.title))
72
+ // ['Interstellar', 'Inception', 'The Dark Knight'...]
73
+ ```
74
+
75
+ ---
76
+
77
+ ## How it works
78
+
79
+ ```
80
+ Your database or JSON files
81
+ ↓ linklab build
82
+ {alias}.json (compiled graph — precalculated routes)
83
+ ↓ QueryEngine
84
+ SQL generated automatically
85
+ ↓ NavigationEngine
86
+ Fluent API: cinema.directors('Nolan').movies.actors
87
+ ```
88
+
89
+ `linklab build` is a CLI command from `@linklabjs/cli`. It produces the compiled graph that `@linklabjs/core` consumes at runtime.
90
+
91
+ ---
92
+
93
+ ## Semantic views
94
+
95
+ When the same entity appears in multiple roles — actors, directors, writers all being `people` — LinkLab detects this at compile time and generates semantic views automatically:
96
+
97
+ ```
98
+ netflix.movies(278).people → everyone (all roles)
99
+ netflix.movies(278).actors → actors only
100
+ netflix.movies(278).director → director only
101
+ netflix.movies(278).writers → writers only
102
+ ```
103
+
104
+ `people('Christopher Nolan').director` and `directors('Christopher Nolan')` are equivalent — same entity, filtered by role. No separate endpoint to maintain.
105
+
106
+ ---
107
+
108
+ ## API levels
109
+
110
+ ```
111
+ Level 1 cinema.directors('Nolan').movies.actors
112
+ → semantic facade, transparent, 80% of use cases
113
+
114
+ Level 2 graph.from('Pigalle').to('Alesia').path(Strategy.Shortest)
115
+ → paths, strategies, Dijkstra
116
+
117
+ Level 3 graph.entities / .relations / .weights
118
+ → introspection, debug, dashboards
119
+
120
+ Level 4 graph.weight(edge).set(value) / .compile()
121
+ → metaprogramming, CalibrationJob
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Level 1 — Semantic facade
127
+
128
+ ### `new Graph(compiledGraph, options?)` → `Graph`
129
+
130
+ Main entry point. Builds a navigable graph.
131
+
132
+ ```typescript
133
+ import { Graph } from '@linklabjs/core'
134
+
135
+ const graph = new Graph(compiledGraph, {
136
+ compiled?: CompiledGraph, // precalculated routes
137
+ dataset?: Record<string, any[]>, // JSON data in memory
138
+ provider?: Provider, // PostgresProvider for real database
139
+ })
140
+ ```
141
+
142
+ ### `graph.domain()` → `DomainProxy`
143
+
144
+ Returns the transparent semantic proxy (Level 1).
145
+
146
+ ```typescript
147
+ const cinema = graph.domain()
148
+
149
+ const cast = await cinema.movies(278).people
150
+ const films = await cinema.directors('Nolan').movies
151
+ const found = await cinema.movies({ title: 'Inception' })
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Level 2 — Pathfinding
157
+
158
+ ### `graph.from(nodeId)` → `PathBuilder`
159
+
160
+ ```typescript
161
+ const builder = graph.from('Pigalle').to('Alesia')
162
+
163
+ builder.paths() // all paths — Shortest by default
164
+ builder.paths(Strategy.Comfort()) // +8 min per transfer
165
+ builder.path() // best path only
166
+ builder.links // subgraph between two nodes
167
+ ```
168
+
169
+ ### `Strategy`
170
+
171
+ ```typescript
172
+ import { Strategy } from '@linklabjs/core'
173
+
174
+ Strategy.Shortest() // minimal raw weight (default)
175
+ Strategy.Comfort() // +8 min per transfer
176
+ Strategy.Custom(penalty) // +penalty per transfer
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Level 3 — Introspection
182
+
183
+ ```typescript
184
+ graph.entities // GraphNode[] — all nodes
185
+ graph.relations // GraphEdge[] — all edges
186
+ graph.schema // Record<string, string> — node types
187
+ graph.weights // Map<string, number> — current weights
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Fastify plugin — REST + HATEOAS
193
+
194
+ ```typescript
195
+ import Fastify from 'fastify'
196
+ import { linklabPlugin } from '@linklabjs/core'
197
+
198
+ const app = Fastify()
199
+
200
+ await app.register(linklabPlugin, {
201
+ graph: compiledGraph,
202
+ prefix: '/api',
203
+ dataLoader: { provider: postgresProvider },
204
+ onEngine: (engine, req) => {
205
+ engine.hooks.on('access.check', async (ctx) => {
206
+ if (!req.user) return { cancelled: true, reason: 'unauthenticated' }
207
+ })
208
+ }
209
+ })
210
+
211
+ // These routes work automatically — no configuration:
212
+ // GET /api/movies
213
+ // GET /api/movies/278
214
+ // GET /api/movies/278/people
215
+ // GET /api/directors/2/movies
216
+ ```
217
+
218
+ Response includes `_links` generated from the graph:
219
+
220
+ ```json
221
+ {
222
+ "id": 504,
223
+ "name": "Tim Robbins",
224
+ "_links": {
225
+ "self": { "href": "/api/movies/278/people/504" },
226
+ "up": { "href": "/api/movies/278" },
227
+ "movies": { "href": "/api/movies/278/people/504/movies" },
228
+ "credits": { "href": "/api/movies/278/people/504/credits" }
229
+ }
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Low-level API
236
+
237
+ ### `QueryEngine`
238
+
239
+ ```typescript
240
+ import { QueryEngine } from '@linklabjs/core'
241
+
242
+ const engine = new QueryEngine(compiledGraph)
243
+
244
+ engine.getRoute(from, to) // RouteInfo
245
+ engine.generateSQL(options: QueryOptions) // string — readable SQL
246
+ engine.executeInMemory(options, dataset) // any[] — JSON execution
247
+ engine.generateJSONPipeline(options) // object — debug
248
+ ```
249
+
250
+ ```typescript
251
+ interface QueryOptions {
252
+ from: string
253
+ to: string
254
+ filters?: Record<string, any> // WHERE conditions
255
+ semantic?: string // semantic view label — ex: 'actor'
256
+ }
257
+ ```
258
+
259
+ ### `PathFinder`
260
+
261
+ ```typescript
262
+ import { PathFinder } from '@linklabjs/core'
263
+
264
+ const finder = new PathFinder(graph)
265
+
266
+ finder.findShortestPath(from, to) // PathDetails | null
267
+ finder.findAllPaths(from, to, maxPaths?) // Path[]
268
+ finder.hasPath(from, to) // boolean
269
+ finder.getReachableNodes(from, maxDepth?) // Set<string>
270
+ finder.getPathWeight(path) // number
271
+ finder.getStats() // { nodes, edges, avgDegree }
272
+ ```
273
+
274
+ ### `GraphCompiler`
275
+
276
+ ```typescript
277
+ import { GraphCompiler } from '@linklabjs/core'
278
+
279
+ const compiler = new GraphCompiler({
280
+ weightThreshold?: number, // pruning threshold (default: 1000)
281
+ keepFallbacks?: boolean, // keep alternative routes
282
+ maxFallbacks?: number, // max alternatives per route
283
+ })
284
+
285
+ compiler.compile(graph, metrics): CompiledGraph
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Core types
291
+
292
+ ```typescript
293
+ interface GraphNode {
294
+ id: string
295
+ type: string
296
+ label?: string
297
+ [key: string]: any
298
+ }
299
+
300
+ interface GraphEdge {
301
+ from: string
302
+ to: string
303
+ weight: number
304
+ name?: string
305
+ via?: string
306
+ metadata?: Record<string, any>
307
+ }
308
+
309
+ interface CompiledGraph {
310
+ version: string
311
+ compiledAt: string
312
+ nodes: GraphNode[]
313
+ routes: RouteInfo[]
314
+ }
315
+
316
+ interface RouteInfo {
317
+ from: string
318
+ to: string
319
+ semantic?: boolean
320
+ label?: string
321
+ primary: {
322
+ path: string[]
323
+ edges: RouteStep[]
324
+ weight: number
325
+ joins: number
326
+ }
327
+ fallbacks: RouteInfo['primary'][]
328
+ }
329
+
330
+ interface Provider {
331
+ query<T>(sql: string, params?: any[]): Promise<T[]>
332
+ close(): Promise<void>
333
+ }
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Recommended imports
339
+
340
+ ```typescript
341
+ import {
342
+ Graph,
343
+ Strategy,
344
+ PathFinder,
345
+ QueryEngine,
346
+ GraphCompiler,
347
+ NavigationEngine,
348
+ linklabPlugin,
349
+ } from '@linklabjs/core'
350
+
351
+ import type {
352
+ GraphNode,
353
+ GraphEdge,
354
+ CompiledGraph,
355
+ RouteInfo,
356
+ QueryOptions,
357
+ } from '@linklabjs/core'
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Examples
363
+
364
+ | Example | Source | Demonstrates |
365
+ |---------|--------|-------------|
366
+ | `dvdrental` | PostgreSQL | FK relations, semantic views, full pipeline |
367
+ | `netflix` | JSON | Pivot detection, semantic views (actors/directors/writers) |
368
+ | `cinema` | JSON | Minimal graph, REPL starting point |
369
+ | `metro` | GTFS open data | Dijkstra, real RATP weights, strategies |
370
+ | `musicians` | Manual | Cycles, minHops, via filter |
371
+
372
+ See the [examples](./src/examples) folder.
373
+
374
+ ---
375
+
376
+ ## Custom formatters
377
+
378
+ Extend `BaseFormatter` to transform raw navigation results into domain-readable output:
379
+
380
+ ```typescript
381
+ import { BaseFormatter } from '@linklabjs/core'
382
+ import type { NavigationPath } from '@linklabjs/core'
383
+
384
+ export class MyFormatter extends BaseFormatter {
385
+ format(path: NavigationPath): string {
386
+ return [
387
+ `Path: ${path.nodes.join(' → ')}`,
388
+ `Weight: ${path.totalWeight}`,
389
+ ].join('\n')
390
+ }
391
+ }
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Not an ORM
397
+
398
+ LinkLab does not map tables to objects. It does not manage migrations. It does not hide your SQL.
399
+
400
+ It compiles a navigation graph from your existing schema and resolves paths through it. The generated SQL is readable — visible in the REPL and in `QueryEngine.generateSQL()`.
401
+
402
+ ---
403
+
404
+ - [GitHub](https://github.com/charley-simon/linklab)
405
+ - [Report an issue](https://github.com/charley-simon/linklab/issues)
406
+
407
+ ---
408
+
409
+ ## License
410
+
411
+ MIT — Charley Simon
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@linklabjs/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "LinkLab core — semantic navigation graph engine",
6
+ "author": "Charley Simon",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/charley-simon/linklab",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/charley-simon/linklab.git",
12
+ "directory": "packages/core"
13
+ },
14
+ "keywords": ["linklab", "graph", "navigation", "hateoas", "rest", "api", "semantic", "trail", "typescript"],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": ["dist", "src"],
24
+ "scripts": {
25
+ "build": "pnpm run clean && tsc -p tsconfig.json",
26
+ "clean": "rimraf --glob dist \"src/**/*.js\" \"src/**/*.d.ts\" \"tests/**/*.js\"",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "prepublishOnly": "pnpm run build"
31
+ },
32
+ "dependencies": {
33
+ "chalk": "^5.6.2",
34
+ "dotenv": "^17.3.1",
35
+ "fastify": "^5.8.2",
36
+ "fastify-plugin": "^5.1.0",
37
+ "pg": "^8.20.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.4.0",
41
+ "@vitest/coverage-v8": "^2.0.0",
42
+ "@vitest/ui": "^2.0.0",
43
+ "rimraf": "^6.1.3",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^2.0.0"
46
+ },
47
+ "engines": { "node": ">=18" }
48
+ }