@_linked/server 3.13.0 → 3.14.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 (41) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/lib/esm/backend.d.ts +23 -5
  3. package/lib/esm/backend.js +116 -25
  4. package/lib/esm/backend.js.map +1 -1
  5. package/lib/esm/shapes/LincdAPI.d.ts +1 -1
  6. package/lib/esm/shapes/LincdAPI.js +43 -7
  7. package/lib/esm/shapes/LincdAPI.js.map +1 -1
  8. package/lib/esm/shapes/LinkedServer.d.ts +50 -37
  9. package/lib/esm/shapes/LinkedServer.js +121 -76
  10. package/lib/esm/shapes/LinkedServer.js.map +1 -1
  11. package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +35 -6
  12. package/lib/esm/shapes/quadstores/RemoteDataset.js +77 -12
  13. package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
  14. package/lib/esm/utils/adminChannel.d.ts +22 -51
  15. package/lib/esm/utils/adminChannel.js +48 -36
  16. package/lib/esm/utils/adminChannel.js.map +1 -1
  17. package/lib/esm/utils/effectiveRoutes.d.ts +6 -0
  18. package/lib/esm/utils/effectiveRoutes.js +2 -1
  19. package/lib/esm/utils/effectiveRoutes.js.map +1 -1
  20. package/lib/esm/utils/forwardedTransactions.d.ts +46 -0
  21. package/lib/esm/utils/forwardedTransactions.js +155 -0
  22. package/lib/esm/utils/forwardedTransactions.js.map +1 -0
  23. package/lib/esm/utils/graphTargets.d.ts +37 -20
  24. package/lib/esm/utils/graphTargets.js +61 -82
  25. package/lib/esm/utils/graphTargets.js.map +1 -1
  26. package/lib/esm/utils/ontologySync.d.ts +40 -23
  27. package/lib/esm/utils/ontologySync.js +59 -37
  28. package/lib/esm/utils/ontologySync.js.map +1 -1
  29. package/lib/esm/utils/queryPlane.d.ts +13 -0
  30. package/lib/esm/utils/queryPlane.js +49 -2
  31. package/lib/esm/utils/queryPlane.js.map +1 -1
  32. package/lib/esm/utils/reloadSource.js +1 -1
  33. package/lib/esm/utils/reloadSource.js.map +1 -1
  34. package/lib/esm/utils/serverTransactions.d.ts +22 -0
  35. package/lib/esm/utils/serverTransactions.js +81 -0
  36. package/lib/esm/utils/serverTransactions.js.map +1 -0
  37. package/lib/esm/utils/syncShapes.d.ts +120 -8
  38. package/lib/esm/utils/syncShapes.js +151 -32
  39. package/lib/esm/utils/syncShapes.js.map +1 -1
  40. package/package.json +2 -2
  41. package/readme.md +134 -24
package/readme.md CHANGED
@@ -25,6 +25,7 @@ If you want to further adjust the functionality of LincdServer yourself, either
25
25
  - [Without `url`](#without-url)
26
26
  - [With `url`](#with-url)
27
27
  - [`ping`](#ping)
28
+ - [Store transactions](#store-transactions)
28
29
  - [Shape catalog per request](#shape-catalog-per-request)
29
30
  - [API-only servers (`server.apiOnly`)](#api-only-servers-serverapionly)
30
31
  - [Host control channel (`/admin/*`)](#host-control-channel-admin)
@@ -314,7 +315,7 @@ Export path `@_linked/server/shapes/quadstores/RemoteDataset` (named export `Rem
314
315
  | Answered by | this app's own backend | a remote data server, e.g. a host's gateway |
315
316
  | Sends through | `Server.call`'s package form (`/call/@_linked/server/<method>`); on a backend, `Server`'s local shortcut | `globalThis.fetch` to `{url}/call/@_linked/server/<method>`, never `Server` |
316
317
  | Authenticated by | the user's session | `Authorization: Bearer <token>` |
317
- | `init()` | resolves at once | posts `ping`, throws a typed error when the server cannot be used |
318
+ | `init()` | asks the backend's `ping` for transactions only, resolves `undefined` (a failed ping leaves them off) | posts `ping`, throws a typed error when the server cannot be used |
318
319
  | Typical config | `linked.frontend.datasets.json` | `linked.backend.datasets.json` |
319
320
 
320
321
  ```typescript
@@ -349,10 +350,16 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
349
350
  ```
350
351
 
351
352
  - Each method posts `{"args": [<query.toJSON()>]}` to `{url}/call/@_linked/server/<method>`
352
- (`selectQuery`, which also carries counts, `askQuery`, `updateQuery`, `createQuery`,
353
- `deleteQuery`) with `Content-Type` and `Accept` set to `application/json` and, when `token` is
354
- non-empty, `Authorization: Bearer <token>`. It resolves the parsed body; `null` is a valid
355
- result. Nothing is retried, so a write is never applied twice.
353
+ (`readQuery` for every read: a select, a count or an ask, told apart by the DSL-JSON `op`;
354
+ `updateQuery`, `createQuery`, `deleteQuery`; `graphQuery` for a graph query) with
355
+ `Content-Type` and `Accept` set to `application/json` and, when `token` is non-empty,
356
+ `Authorization: Bearer <token>`. It resolves the parsed body; `null` is a valid result. Nothing
357
+ is retried, so a write is never applied twice. The store declares `supports: ['ask']` (plus
358
+ `'transactions'`, below); a triples or terms read is refused without being sent, as the
359
+ server's `readQuery` answers neither.
360
+ - The server's `readQuery` checks a read under the access rule of its kind, read from the body
361
+ before it is decoded: a select and a count under `select`, an ask under `ask`. A triples or terms
362
+ read, a graph query and a write posted to it answer 400.
356
363
  - **The user's token.** When the query is made while a signed-in user's HTTP request is being
357
364
  answered (the call context is `http`), the post asks the hook on
358
365
  `globalThis[Symbol.for('@_linked/auth:userTokenHeaders')]` (exported as
@@ -387,9 +394,9 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
387
394
  `init()`, so a boot that must fail loudly awaits it itself before registering the store.
388
395
  - `init()` resolves the ping body, which the store also keeps as the readonly `info`
389
396
  (`RemoteDatasetPingInfo`, frozen; `{}` when the body is not a JSON object; `undefined` before
390
- a successful ping and in the no-url form). The generic ping answers `{ok: true}`; a gateway may
391
- add fields naming the dataset it serves (CN's adds `projectId`, `branchName`, `dataset`,
392
- `forked`), so a boot can log where its data goes:
397
+ a successful ping and in the no-url form). The generic ping answers `{ok: true, supports}`; a
398
+ gateway may add fields naming the dataset it serves (CN's adds `projectId`, `branchName`,
399
+ `slot`, `dataset`, `forked`), so a boot can log where its data goes:
393
400
 
394
401
  ```ts
395
402
  const info = await store.init();
@@ -413,9 +420,43 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
413
420
  ### `ping`
414
421
 
415
422
  `POST /call/@_linked/server/ping` (a `public` method of this package's generic provider) reads
416
- nothing, changes nothing and answers `{ "ok": true }`. A plain linked server has no token
417
- concept; a host that serves the wire with tokens checks the token itself and answers 401 for a
418
- refused one.
423
+ nothing, changes nothing and answers `{ "ok": true, "supports": [] }`, or `"supports":
424
+ ["transactions"]` when the server holds store transactions (below). A plain linked server has no
425
+ token concept; a host that serves the wire with tokens checks the token itself and answers 401
426
+ for a refused one.
427
+
428
+ ### Store transactions
429
+
430
+ Core's store transactions (`LinkedStorage.begin()`, `LinkedStorage.transaction(fn)`) go through a
431
+ `RemoteDataset` and this server to the stores behind it:
432
+
433
+ ```
434
+ POST /call/@_linked/server/beginTransaction {"args": []} → "<txId>"
435
+ POST /call/@_linked/server/commit {"args": ["<txId>"]} → null
436
+ POST /call/@_linked/server/rollback {"args": ["<txId>"]} → null
437
+ POST /call/@_linked/server/<x>Query {"args": [<query.toJSON(), with "txId">]}
438
+ ```
439
+
440
+ - **`RemoteDataset`** declares `'transactions'` in `supports` only after a `ping` that listed
441
+ them (both forms), so a server that does not hold them is never sent a txId; until then core
442
+ warns once per store and runs a transaction's queries directly. `beginTransaction()`,
443
+ `commit(txId)` and `rollback(txId)` post to the methods of the same names, never retried.
444
+ - **The server** holds transactions only when every dataset `LinkedStorage` routes to declares
445
+ `'transactions'` (`ping` says so). It answers a txId of its own and keeps a core `Transaction`
446
+ for it, which opens a transaction in each store a query of it routes to (a second store gets
447
+ its own, with core's warning). A query carrying the txId is checked against the store rules as
448
+ if it were sent alone; a refused or failed one rolls the transaction back in every store. An
449
+ idle transaction is rolled back after 30 seconds.
450
+ - **The caller.** A transaction needs a session (401 without one) and is bound to the session's
451
+ user account; a backend-to-backend call with no request behind it is `system`. Anyone else's
452
+ request with that txId is refused and leaves the transaction as it was.
453
+ - **Refusals** (`{error, code}`): 501 `transactions-unsupported`, 409 `transaction-unknown` (never
454
+ begun, ended, failed or expired; a `rollback` of one resolves), 403 `transaction-not-yours`. A
455
+ query with a txId sent to `/api/*`, and a graph query with one, are refused (400). Any refusal
456
+ of a request that names the caller's own transaction (a body that does not decode included)
457
+ rolls that transaction back. A store's own refusal is passed back as for any failed query.
458
+ - A forwarder of its own (a gateway) does the same: it lists `'transactions'` only when the store
459
+ it forwards to does, and never runs a query that carries a txId outside its transaction.
419
460
 
420
461
  ### Shape catalog per request
421
462
 
@@ -468,10 +509,8 @@ A host that starts the app process controls it over HTTP routes on the app's own
468
509
  | `GET /admin/health` | 200 `{status: 'ok'}` once the server listens (the boot ontology sync, shape check and shape sync have finished by then) |
469
510
  | `POST /admin/restart` | 202, then the process exits; the host starts it again |
470
511
  | `POST /admin/graph-registry-changed` | body `{dataset}`: the host changed that dataset's named graphs; the process drops its cached graph registry and answers 200 `{invalidated: dataset}`; 400 without a dataset name |
471
- | `GET /admin/routes` | 200 `{datasets: [{store, id?, default, routes, defaultGraphShapes}], graphTargets, graphTargetEnv}`: the effective graph routes and graph targets (see below) |
472
- | `POST /admin/sync-shapes` | 200 `{written, orphanScope, shapeGraphs, reloaded, shapes, ontologies: {written, skipped, graphs, droppedGraphs}, shapeCheck: {errors, warnings}}` (or `{written: 0, skipped, …}` when shape sync is off); 400 `{error}` for a malformed body or a package it may not reload; 500 `{error}` when the sync fails or times out |
473
-
474
- `POST /admin/sync-shapes` runs the ontology sync and the shape check (see [Ontology sync and the shape check](#ontology-sync-and-the-shape-check)), then re-syncs the registered shapes into the app's data exactly as the boot does (see [Shape graphs](#shape-graphs): only graphs whose stored content hash differs are written, with the boot's orphan policy and `syncShapesOnBoot` / `LINKED_SYNC_SHAPES_ON_BOOT` switches). `written` counts the shape graphs written, and `shapeGraphs` reports them. The optional body `{packages: [...]}` names packages whose source the host changed; each must be a valid npm package name and the app itself or one of its direct dependencies, never an `@_linked/*` package; the list only says what to reload, it never filters the syncs. In dev their modules are invalidated in Vite and their backends reloaded before the sync, so a shape file written a moment ago is registered; a built app reloads nothing. `reloaded` lists the packages actually reloaded and `shapes` the node shape IRIs registered afterwards, so the host can check that the shape it wrote is there. Syncs run one at a time, as the system; one that takes longer than `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120000) fails and releases the queue. A source reload by itself (HMR) never syncs shapes. A failed ontology sync does not stop the shape sync; the route then answers 500 with the ontology error.
512
+ | `GET /admin/routes` | 200 `{datasets: [{store, id?, default, routes, defaultGraphShapes}], graphTargets, graphTargetEnv, graphWriter}`: the effective graph routes, graph targets and graph writer (see below) |
513
+ | `POST /admin/graph-content` | body `{packages?, reload?}`: 200 `{graphTargets, packages: [{name, shapes?, ontology?}], appGraph?, withoutTurtle, unreadable, reloaded}`, what the syncs would write, writing nothing, after reloading the packages named in `reload` (see [Graph writer and graph content](#graph-writer-and-graph-content)); 400 `{error}` for a malformed body or a package it may not reload; 409 `{error}` when a graph target is not `package`; 500 `{error}` when it fails or times out |
475
514
 
476
515
  `GET /admin/routes` reports the graph routes the process runs with, as core's routing holds them once the app's storage file (`linked.backend.storage`) has run. Routes are written only there (`LinkedStorage.setDatasetForShapes(dataset, shapes, {graph})`); the app never pushes them anywhere, and this route lets a host check what actually runs. One entry per dataset the router knows, the default dataset first, then in the order they were pinned:
477
516
 
@@ -480,7 +519,7 @@ A host that starts the app process controls it over HTTP routes on the app's own
480
519
  - `routes`: `{<shape IRI>: <graph IRI>}` as registered. A sub-shape without a route of its own is written to its nearest routed super-shape's graph and is not listed.
481
520
  - `defaultGraphShapes`: the registered shapes that resolve to this dataset and are written to its default graph (no route of their own, no routed super-shape), sorted.
482
521
 
483
- `graphTargets` is the process's one graph-target setting, `{ontologyGraph, shapeGraph}` (`package`, `default` or a shared graph IRI each; see [Ontology and shape graphs](#ontology-and-shape-graphs)): where the boot syncs write, for every dataset. `graphTargetEnv` holds `LINKED_ONTOLOGY_GRAPH` / `LINKED_SHAPE_GRAPH` as the process has them (unset ones left out), so a host can tell where the setting came from.
522
+ `graphWriter` is `LINKED_GRAPH_WRITER` (`app` or `host`; see [Graph writer and graph content](#graph-writer-and-graph-content)). `graphTargets` is the process's one graph-target setting, `{ontologyGraph, shapeGraph}` (`package`, `default` or a shared graph IRI each; see [Ontology and shape graphs](#ontology-and-shape-graphs)): where the boot syncs write, for every dataset. `graphTargetEnv` holds `LINKED_ONTOLOGY_GRAPH` / `LINKED_SHAPE_GRAPH` as the process has them (unset ones left out), so a host can tell where the setting came from.
484
523
 
485
524
  ```json
486
525
  {"datasets": [{"store": "FusekiStore", "default": true,
@@ -492,23 +531,67 @@ A host that starts the app process controls it over HTTP routes on the app's own
492
531
 
493
532
  Side effects to expect:
494
533
 
495
- - **A reload briefly disposes the package's providers.** Reloading a package (on HMR, or for `sync-shapes`) runs `dispose()` on its providers and constructs new ones; a request to that package's routes or methods that arrives in between can fail. Reloads run one at a time; one that takes longer than `LINKED_SOURCE_RELOAD_TIMEOUT_MS` (default 60000) rejects and releases the queue.
496
- - **A restart drops in-flight requests.** `POST /admin/restart` exits the process 50 ms after answering. A host that restarts an app whose server has no `sync-shapes` route (an older `@_linked/server`) to make its boot sync the shapes drops whatever that app was serving.
534
+ - **A reload briefly disposes the package's providers.** Reloading a package (on HMR, or for `graph-content`'s `reload`) runs `dispose()` on its providers and constructs new ones; a request to that package's routes or methods that arrives in between can fail. Reloads run one at a time; one that takes longer than `LINKED_SOURCE_RELOAD_TIMEOUT_MS` (default 60000) rejects and releases the queue.
535
+ - **A restart drops in-flight requests.** `POST /admin/restart` exits the process 50 ms after answering.
497
536
  - The channel listens on the app's public port, like every other route; only the secret protects it.
498
537
 
538
+ ### Graph writer and graph content
539
+
540
+ `LINKED_GRAPH_WRITER` says who writes the app's shape and ontology graphs: `app` (the default) or
541
+ `host`. Any other value stops the boot, naming the variable. Under `host` (a host such as Create
542
+ Now writes them) the app writes no shape or ontology graph: the boot registers the ontologies,
543
+ loads the class hierarchy and runs the shape check but syncs nothing (it logs one line saying the
544
+ host writes the graphs). Under `app` the boot syncs them (see [Ontology sync and the shape
545
+ check](#ontology-sync-and-the-shape-check) and [Shape graphs](#shape-graphs)) unless `linked.config`
546
+ sets `syncShapesOnBoot: false`.
547
+
548
+ The host reads the graphs from `POST /admin/graph-content`, body `{packages?: string[], reload?:
549
+ string[]}`. `reload` names packages whose source the host changed; each must be a valid npm
550
+ package name and the app itself or one of its direct dependencies (its `linked.unpublishedPackages`
551
+ among them), never an `@_linked/*` package. In dev their modules are invalidated in Vite and their
552
+ backends reloaded first, then the ontologies are registered again and the shape check runs, so a
553
+ shape file written a moment ago is in the answer; a built app reloads nothing. `reloaded` lists the
554
+ packages actually reloaded. `packages` limits the answer; without it every registered package with
555
+ shapes or ontology data is answered. It writes nothing. Requests run one at a time, as the system;
556
+ one that takes longer than `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120000) fails with 500 and
557
+ releases the queue. It answers:
558
+
559
+ ```json
560
+ {"graphTargets": {"ontologyGraph": "package", "shapeGraph": "package"},
561
+ "packages": [{"name": "@acme/music",
562
+ "shapes": {"graph": "https://linked.cm/pkg/acme-music/graph/shapes", "mediaType": "text/turtle",
563
+ "text": "@prefix …", "hash": "…", "sources": ["data/shapes.ttl"]},
564
+ "ontology": {"graph": "https://linked.cm/pkg/acme-music/graph/ontology",
565
+ "mediaType": "application/n-triples", "text": "<…> <…> <…> .\n", "hash": "…",
566
+ "sources": ["data/music.ttl"]}}],
567
+ "appGraph": {"graph": "https://acme.example/data/graph/shapes", "mediaType": "text/turtle",
568
+ "text": "…", "hash": "…", "sources": []},
569
+ "withoutTurtle": [], "unreadable": [], "reloaded": []}
570
+ ```
571
+
572
+ Each `text` and `hash` is exactly what the `package`-mode sync writes and records as the graph's
573
+ `core:contentHash` (a shape graph: its Turtle and `shapeGraphHash`; an ontology graph: the
574
+ N-Triples of the package's data files and `ontologyGraphHash` of their raw texts, so the host must
575
+ not hash the text itself), and `sources` the record's `dcterms:source`. A package's shape Turtle
576
+ comes from where the shape sync takes it: an installed package without `shapes.ttl` has no `shapes`
577
+ here either and is listed in `withoutTurtle`. `unreadable` lists packages whose ontology data could
578
+ not be read. `appGraph` is `null` when the app has no single-owner shapes (drop the graph) and left
579
+ out without `DATA_ROOT`. The route answers 409 unless both `LINKED_ONTOLOGY_GRAPH` and
580
+ `LINKED_SHAPE_GRAPH` are `package`.
581
+
499
582
  ### Ontology sync and the shape check
500
583
 
501
- Before its shape sync, at boot and on `POST /admin/sync-shapes`, the server:
584
+ Before its shape sync at boot, the server:
502
585
 
503
586
  1. **Registers the installed packages' ontologies** (core `registerInstalledOntologies`): it imports `lib/esm/ontologies/*.register.js` of every linked package it discovered in the app's dependency tree, so a package the app only deep-imports shape files from (`@_linked/schema/shapes/Person`) is synced and checked too, and of every workspace package of the app (`linkedPackage: true` under its `workspaces`), which no package may import (a Create Now clone's `packages/<slug>-ont`). In dev, a package without a built `lib/` registers from `src/ontologies/*.register.ts` through Vite's `ssrLoadModule`; a built app skips it. A register module that fails is logged and the rest go on.
504
- 2. **Syncs the ontologies** (core `syncOntologies`): every registered package's ontology data is written into the app's default dataset, where `LINKED_ONTOLOGY_GRAPH` says (one graph per package by default; see [Ontology and shape graphs](#ontology-and-shape-graphs)), so the store's RDFS view infers the class hierarchy. The log line names a shared graph or the default graph when the sync wrote there. A package whose stored hash matches is skipped, so an unchanged sync writes nothing. `ontologies.written` and `ontologies.skipped` name the packages (a package whose data cannot be read is skipped with core's warning). Sync nodes of packages no longer registered are pruned only with `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, the shape sync's orphan policy.
505
- 3. **Checks the shapes** against that hierarchy (core `checkShapeHierarchy`, with every package's `dependencies` and `peerDependencies`). A running app is never stopped by it: each finding is logged, and `shapeCheck` counts them by severity.
587
+ 2. **Syncs the ontologies** (core `syncOntologiesRouted`): every registered package's ontology data is written into each dataset the storage routing needs it in (see [Which datasets the syncs write to](#which-datasets-the-syncs-write-to)), where `LINKED_ONTOLOGY_GRAPH` says (one graph per package by default; see [Ontology and shape graphs](#ontology-and-shape-graphs)), so the store's RDFS view infers the class hierarchy. The log line names a shared graph or the default graph when the sync wrote there. A package whose stored hash matches is skipped, so an unchanged sync writes nothing. The log line names the packages written and skipped (a package whose data cannot be read is skipped with core's warning). Sync nodes of packages no longer registered are pruned only with `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, the shape sync's orphan policy.
588
+ 3. **Checks the shapes** against that hierarchy (core `checkShapeHierarchy`, with every package's `dependencies` and `peerDependencies`). A running app is never stopped by it: each finding is logged, with a count by severity.
506
589
 
507
- The ontology sync follows the shape sync's switches (`syncShapesOnBoot`, `LINKED_SYNC_SHAPES_ON_BOOT`), at boot and on `POST /admin/sync-shapes` alike: with shape sync off, the route syncs neither ontologies nor shapes (it still registers the packages' ontologies and runs the shape check). `LINKED_SYNC_ONTOLOGIES_ON_BOOT=false` turns off only the boot's ontology sync. The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded at boot either way. A failed ontology sync at boot is logged and the boot goes on.
590
+ The ontology sync and the shape sync have one switch: both run when the app writes its own graphs (`LINKED_GRAPH_WRITER` unset or `app`; see [Graph writer and graph content](#graph-writer-and-graph-content)) and `linked.config` does not set `syncShapesOnBoot: false`. With the sync off the boot still registers the packages' ontologies and runs the shape check, and `POST /admin/graph-content` with `reload` registers and checks again. The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded at boot either way. A failed ontology sync at boot is logged and the boot goes on.
508
591
 
509
592
  ### Shape graphs
510
593
 
511
- After the ontology sync and the shape check, at boot and on `POST /admin/sync-shapes`, the server syncs the registered shapes into the app's default dataset with core's `syncShapeGraphs`. This section describes the default, `LINKED_SHAPE_GRAPH=package`; with a shared graph IRI every package's shapes and the app's generated ones go into that one graph (`graphs` lists it, `appGraph.graph` is it), and with `default` the shapes go into the default graph (`defaultGraph` counts the writes). See [Ontology and shape graphs](#ontology-and-shape-graphs).
594
+ After the ontology sync and the shape check, at boot, the server syncs the registered shapes with core's `syncShapeGraphsRouted`, into each dataset the storage routing needs them in (see [Which datasets the syncs write to](#which-datasets-the-syncs-write-to)). This section describes the default, `LINKED_SHAPE_GRAPH=package`; with a shared graph IRI every package's shapes and the app's generated ones go into that one graph (`graphs` lists it, `appGraph.graph` is it), and with `default` the shapes go into the default graph (`defaultGraph` counts the writes). See [Ontology and shape graphs](#ontology-and-shape-graphs).
512
595
 
513
596
  - **One graph per package.** Each package's shapes go in `{package IRI}/graph/shapes`, with a `ShapesGraph` record in the graph registry (never a hierarchy source). A graph whose stored content hash matches is not written, so an unchanged sync writes nothing; a changed one is replaced whole.
514
597
  - **Where the Turtle comes from.** An installed package's built `lib/esm/data/shapes.ttl`, which `linked build` writes. An installed package without that file gets no shape graph: nothing is generated for it (a package with no shapes has none). The app's own packages (the app and its workspace packages) are generated in process: in dev through Vite's `ssrLoadModule` (core's `packageShapesTurtle` on the core the source shapes registered on, the same bytes `linked build` writes); in a built app from their `lib/esm/data/shapes.ttl` when they have one.
@@ -522,7 +605,34 @@ The boot logs one line, for example:
522
605
  [LinkedServer] shape graph sync: written @acme/shop; skipped @_linked/schema; app graph written; graphs written https://linked.cm/pkg/acme-shop/graph/shapes, https://acme.example/data/graph/shapes; no shapes.ttl (no shape graph): @acme/legacy
523
606
  ```
524
607
 
525
- `POST /admin/sync-shapes` answers the same as `shapeGraphs`: `{written, skipped, pruned, graphs, appGraph: {graph, status}, withoutTurtle, cleaned, target}` (plus `defaultGraph`, the default-graph sync's writes, under `LINKED_SHAPE_GRAPH=default` or on a store without graph queries). `target` is the process's `shapeGraph` setting (see [Ontology and shape graphs](#ontology-and-shape-graphs)); with `fallback` set the store had no graph queries and the shapes went into the default graph instead. In a shared graph the rebuild covers the packages with Turtle: a package without it is left out (and logged), where the package mode keeps its old graph.
608
+ The sync runs under the switch of [Ontology sync and the shape check](#ontology-sync-and-the-shape-check): the app writes its own graphs and `syncShapesOnBoot` is not `false`. Its report, `{written, skipped, pruned, graphs, appGraph: {graph, status}, withoutTurtle, cleaned, target}` (plus `defaultGraph`, the default-graph sync's writes, under `LINKED_SHAPE_GRAPH=default` or on a store without graph queries), is what the log line shows. `target` is the process's `shapeGraph` setting (see [Ontology and shape graphs](#ontology-and-shape-graphs)); with `fallback` set the store had no graph queries and the shapes went into the default graph instead. In a shared graph the rebuild covers the packages with Turtle: a package without it is left out (and logged), where the package mode keeps its old graph.
609
+
610
+ ### Which datasets the syncs write to
611
+
612
+ The boot syncs follow the app's storage routing (`linked.backend.storage`), through core's
613
+ `@_linked/core/shapes/syncTargets` (its README's "Which datasets the syncs write to" has the rule):
614
+
615
+ - a package's **shape graph** goes to every dataset one of its shapes routes to, and to the
616
+ dataset(s) the `sh:NodeShape` / `sh:PropertyShape` shapes route to;
617
+ - a package's **ontology graph** goes to every dataset one of its shapes routes to, wherever a
618
+ shape targeting one of its classes routes, and to the dataset(s) of the ontology term shapes
619
+ (`rdfs:Class`, `owl:Class`, `rdf:Property`, …), so the hierarchy is where the instances are;
620
+ - the app's **own shape graph** goes to every dataset a shape it constrains routes to (the shapes
621
+ declaring `contains` properties and their sub-shapes), and those of the shape shapes.
622
+
623
+ An app without pins writes everything into its default dataset. With pins the boot logs
624
+ one line naming the datasets per graph kind, for example:
625
+
626
+ ```
627
+ [LinkedServer] The boot syncs follow the routing: shape graphs → FusekiStore "app" (every package), FusekiStore "music" (@acme/music); app shape graph → FusekiStore "app"; ontology graphs → FusekiStore "app" (every package), FusekiStore "music" (@acme/music, @_linked/schema).
628
+ ```
629
+
630
+ Each sync then logs one line per dataset (` in FusekiStore "music"`), and its report adds
631
+ `datasets` (each dataset's own report). When a pin moves, a package graph routing no longer sends
632
+ to the **default** dataset is dropped there (`prunedGraphs`, one log line each); one left in
633
+ another dataset stays and is reported (`leftovers`, one warning), since a store several apps share
634
+ may hold another app's copy. A dataset that cannot be read or written fails alone: the others are
635
+ synced, then the sync fails naming it (the boot logs a warning and goes on).
526
636
 
527
637
  ## Provider lifecycle and HMR
528
638