@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.2

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 (191) hide show
  1. package/SKILL.md +305 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/open-lix.d.ts +103 -14
  7. package/dist/open-lix.js +3 -0
  8. package/dist/sqlite/index.js +99 -22
  9. package/dist-engine-src/README.md +18 -0
  10. package/dist-engine-src/src/backend/kv.rs +358 -0
  11. package/dist-engine-src/src/backend/mod.rs +12 -0
  12. package/dist-engine-src/src/backend/testing.rs +658 -0
  13. package/dist-engine-src/src/backend/types.rs +96 -0
  14. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  15. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  16. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  17. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  18. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  19. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  20. package/dist-engine-src/src/cel/context.rs +86 -0
  21. package/dist-engine-src/src/cel/error.rs +19 -0
  22. package/dist-engine-src/src/cel/mod.rs +8 -0
  23. package/dist-engine-src/src/cel/provider.rs +9 -0
  24. package/dist-engine-src/src/cel/runtime.rs +167 -0
  25. package/dist-engine-src/src/cel/value.rs +50 -0
  26. package/dist-engine-src/src/changelog/codec.rs +321 -0
  27. package/dist-engine-src/src/changelog/context.rs +92 -0
  28. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  29. package/dist-engine-src/src/changelog/mod.rs +13 -0
  30. package/dist-engine-src/src/changelog/reader.rs +20 -0
  31. package/dist-engine-src/src/changelog/storage.rs +220 -0
  32. package/dist-engine-src/src/changelog/types.rs +38 -0
  33. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  34. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  35. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  36. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  37. package/dist-engine-src/src/common/error.rs +313 -0
  38. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  39. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  40. package/dist-engine-src/src/common/identity.rs +135 -0
  41. package/dist-engine-src/src/common/metadata.rs +35 -0
  42. package/dist-engine-src/src/common/mod.rs +23 -0
  43. package/dist-engine-src/src/common/types.rs +105 -0
  44. package/dist-engine-src/src/common/wire.rs +222 -0
  45. package/dist-engine-src/src/engine.rs +239 -0
  46. package/dist-engine-src/src/entity_identity.rs +285 -0
  47. package/dist-engine-src/src/functions/context.rs +327 -0
  48. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  49. package/dist-engine-src/src/functions/mod.rs +18 -0
  50. package/dist-engine-src/src/functions/provider.rs +130 -0
  51. package/dist-engine-src/src/functions/state.rs +363 -0
  52. package/dist-engine-src/src/functions/types.rs +37 -0
  53. package/dist-engine-src/src/init.rs +505 -0
  54. package/dist-engine-src/src/json_store/compression.rs +77 -0
  55. package/dist-engine-src/src/json_store/context.rs +129 -0
  56. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  57. package/dist-engine-src/src/json_store/mod.rs +9 -0
  58. package/dist-engine-src/src/json_store/store.rs +236 -0
  59. package/dist-engine-src/src/json_store/types.rs +52 -0
  60. package/dist-engine-src/src/lib.rs +61 -0
  61. package/dist-engine-src/src/live_state/context.rs +2241 -0
  62. package/dist-engine-src/src/live_state/mod.rs +15 -0
  63. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  64. package/dist-engine-src/src/live_state/reader.rs +23 -0
  65. package/dist-engine-src/src/live_state/types.rs +239 -0
  66. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  67. package/dist-engine-src/src/plugin/archive.rs +441 -0
  68. package/dist-engine-src/src/plugin/component.rs +183 -0
  69. package/dist-engine-src/src/plugin/install.rs +637 -0
  70. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  71. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  72. package/dist-engine-src/src/plugin/mod.rs +33 -0
  73. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  74. package/dist-engine-src/src/plugin/storage.rs +74 -0
  75. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  76. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  77. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  78. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  79. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  80. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  81. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  82. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  85. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  86. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  87. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  89. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  90. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  91. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  92. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  93. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  94. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  95. package/dist-engine-src/src/schema/definition.json +157 -0
  96. package/dist-engine-src/src/schema/definition.rs +636 -0
  97. package/dist-engine-src/src/schema/key.rs +206 -0
  98. package/dist-engine-src/src/schema/mod.rs +20 -0
  99. package/dist-engine-src/src/schema/seed.rs +14 -0
  100. package/dist-engine-src/src/schema/tests.rs +739 -0
  101. package/dist-engine-src/src/schema_registry.rs +294 -0
  102. package/dist-engine-src/src/session/context.rs +366 -0
  103. package/dist-engine-src/src/session/create_version.rs +80 -0
  104. package/dist-engine-src/src/session/execute.rs +447 -0
  105. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  106. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  107. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  108. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  109. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  110. package/dist-engine-src/src/session/merge/version.rs +437 -0
  111. package/dist-engine-src/src/session/mod.rs +25 -0
  112. package/dist-engine-src/src/session/switch_version.rs +121 -0
  113. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  114. package/dist-engine-src/src/sql2/classify.rs +147 -0
  115. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  116. package/dist-engine-src/src/sql2/context.rs +307 -0
  117. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  118. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  119. package/dist-engine-src/src/sql2/dml.rs +148 -0
  120. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  121. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  122. package/dist-engine-src/src/sql2/error.rs +196 -0
  123. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  124. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  125. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  126. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  127. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  128. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  129. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  130. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  131. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  132. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  133. package/dist-engine-src/src/sql2/mod.rs +43 -0
  134. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  135. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  136. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  137. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  138. package/dist-engine-src/src/sql2/session.rs +135 -0
  139. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  140. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  141. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  142. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  143. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  144. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  148. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  149. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  150. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  151. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  152. package/dist-engine-src/src/storage/context.rs +356 -0
  153. package/dist-engine-src/src/storage/mod.rs +14 -0
  154. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  155. package/dist-engine-src/src/storage/types.rs +501 -0
  156. package/dist-engine-src/src/storage_bench.rs +3406 -0
  157. package/dist-engine-src/src/test_support.rs +81 -0
  158. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  159. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  160. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  161. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  162. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  163. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  164. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  165. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  166. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  167. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  168. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  169. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  170. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  171. package/dist-engine-src/src/transaction/context.rs +1307 -0
  172. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  173. package/dist-engine-src/src/transaction/mod.rs +11 -0
  174. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  175. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  176. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  177. package/dist-engine-src/src/transaction/types.rs +351 -0
  178. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  179. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  180. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  181. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  182. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  183. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  184. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  185. package/dist-engine-src/src/version/context.rs +52 -0
  186. package/dist-engine-src/src/version/mod.rs +12 -0
  187. package/dist-engine-src/src/version/refs.rs +421 -0
  188. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  189. package/dist-engine-src/src/version/types.rs +21 -0
  190. package/dist-engine-src/src/wasm/mod.rs +60 -0
  191. package/package.json +68 -64
package/SKILL.md CHANGED
@@ -1,64 +1,96 @@
1
1
  ---
2
2
  name: lix-js-sdk
3
- description: Use this skill when building examples, demos, tests, or applications with @lix-js/sdk opening a Lix, registering schemas, writing entities through generated SQL tables, branching state into named versions, merging, and querying the change history.
3
+ description: Use this skill when building examples, demos, tests, or applications with @lix-js/sdk: opening a Lix, registering schemas, writing entities through generated SQL tables, creating named versions, merging, and querying change history.
4
4
  ---
5
5
 
6
6
  # Lix JS SDK Skill
7
7
 
8
8
  ## What Is Lix
9
9
 
10
- Lix is an embeddable version control system. The primary use case is **versioning files of any kind** — `.docx`, CAD drawings, PDFs, JSON catalogs, source code — by parsing them into typed entities on write. Once parsed, the file's contents become rows in your schema tables, and Lix versions, branches, and merges those rows. A registered file plugin owns the parse-and-serialize round trip; the engine handles versioning, branching, three-way merge, and an immutable change journal.
10
+ Lix is an embeddable version control system for structured application state. It gives apps named versions, merge, and an immutable SQL-queryable change journal without asking the app to build those systems from scratch.
11
11
 
12
- Apps can also use the entity layer **directly**, without files in the picture. That's the appealing path for app-first products: define a schema, write rows, get versioning for free. Same engine, same primitives.
12
+ Current `@lix-js/sdk` capabilities:
13
13
 
14
- Every row in every registered schema is a tracked entity, every write happens on a named **version** (Lix's word for "branch"), and `mergeVersion` performs a three-way merge over those entities. The whole engine runs in-process, normally against a local SQLite file.
14
+ - Register JSON schemas as tracked entity tables.
15
+ - Read and write entities through generated SQL tables.
16
+ - Create named versions of state and write/read across versions.
17
+ - Merge one version into the active version.
18
+ - Query `lix_change` for history, audit, activity feeds, and undo-style features.
19
+ - Store files as bytes with `lix_file` and version them like other entities.
15
20
 
16
- **Merge granularity is per-entity (per-row), not per-field.** Two versions editing *different rows* (or different entities of the same schema) merge cleanly. Two versions editing the *same row* — even on disjoint fields — surface as a conflict today. Field-level merge is on the roadmap; until it ships, model your data as multiple small entities (sections, blocks, paragraphs, message keys, line items) when you want concurrent reviewers to compose without conflict.
21
+ Product direction:
17
22
 
18
- **Lix targets non-developers as the primary end-user.** That shapes the vocabulary: what Git calls a "branch" is called a **version** in Lix, because "you can have multiple versions of this document" reads naturally to non-devs while "branch" does not. Use "version" — not "branch" — in user-facing copy, demo names, and example values.
23
+ - Lix is designed to version files of any kind by parsing them into typed entities on write.
24
+ - Parser plugins that turn file contents into app entities are not shipped through the JS SDK yet. Do not promise this behavior in demos. Today, `lix_file` versions bytes, while app entities are modeled directly through registered schemas.
19
25
 
20
- **Where Lix sits next to neighbors:** *not Yjs* (Lix is entity-grained, not character-CRDT), *not Git* (typed rows + a queryable change journal, not text diffs), *not plain SQLite* (branches and merges are primitives, not something you build on top).
26
+ Every row in every registered schema is a tracked entity. Merge granularity is currently per-entity, not per-field: two versions editing different rows merge cleanly; two versions editing the same row conflict, even if the fields are disjoint. Model collaborative domains as many small entities, such as sections, blocks, paragraphs, message keys, or line items.
21
27
 
22
- ## When To Use Lix
28
+ Use Lix vocabulary in user-facing copy. What Git calls a branch is called a **version** in Lix because that language makes sense to non-developers.
23
29
 
24
- Reach for Lix when an app needs branchable, mergeable, auditable structured state:
30
+ ## When To Use This Skill
25
31
 
26
- - **Document / CMS / no-code editors with drafts and review.** A user clicks "Propose changes" → that's `createVersion({ name: "Marketing edit" })`. Reviewers diff via `lix_change`, accept via `mergeVersion`. Like Google Docs "suggesting mode", but durable, branchable, and over typed entities.
27
- - **AI agent sandboxes.** Spawn an agent on its own version, let it mutate freely, the human reviews the diff and merges or discards. Cheap rollback, no shadow tables.
28
- - **Real-time and local-first multiplayer.** Lix already journals every change with author, timestamp, and entity identity — that is exactly what a sync protocol needs. Each peer is a version; sync is merge. Conflicts surface as exceptions instead of silent last-writer-wins.
29
- - **Scenario / what-if branching** in spreadsheets, budgets, OKR planners, pricing tables.
30
- - **Translation / localization workflows.** Each translator works on a version of the message catalog; merges flow back to main.
31
- - **Auditable records** (compliance, clinical, legal). `lix_change` is an immutable journal queryable as SQL — replaces a hand-rolled audit log.
32
+ Use this skill when you need to write or debug consumer code using `@lix-js/sdk`:
32
33
 
33
- Anti-fits: high-throughput OLTP (payments, telemetry, ad bidding), pure analytics ingest, and read-only caches. Lix tracks every write — that is dead weight when there is nothing to version.
34
+ - Opening a persistent `.lix` file.
35
+ - Registering schemas.
36
+ - Writing and reading generated SQL entity tables.
37
+ - Reading `execute()` results.
38
+ - Creating, switching, previewing, and merging versions.
39
+ - Querying history through `lix_change`.
40
+ - Building app demos, examples, smoke tests, or product flows around the SDK.
34
41
 
35
- ## API Surface
42
+ Do not use this skill for raw SQLite access, private engine/wasm internals, SDK publishing, SDK build pipelines, or unreleased file-parser plugin behavior.
43
+
44
+ ## Agent Quick Start
45
+
46
+ 1. Install `@lix-js/sdk` and `better-sqlite3`.
47
+ 2. Open with `createBetterSqlite3Backend({ path })`; do not open `.lix` with raw SQLite.
48
+ 3. Register a schema with `x-lix-key`, `x-lix-version`, `x-lix-primary-key`, and `additionalProperties: false`.
49
+ 4. Write rows through the generated table named by `x-lix-key`.
50
+ 5. Use `<schema>_by_version` plus `lixcol_version_id` for side-by-side version reads/writes.
51
+ 6. Query `lix_change` for audit/history instead of hand-rolling audit tables.
52
+ 7. Wrap `mergeVersion()` in `try/catch` whenever conflicts are possible.
53
+
54
+ ## Core Rules
55
+
56
+ - Use the public `@lix-js/sdk` API only.
57
+ - Use `createBetterSqlite3Backend()` for persistent apps, demos, and tests.
58
+ - Use numbered SQL placeholders: `$1`, `$2`, `$3`; bare `?` is rejected.
59
+ - Use `lix_json($1)` when inserting JSON text into JSON-typed columns.
60
+ - Use stable, namespaced, lowercase schema keys like `acme_section`, not generic names like `task`.
61
+ - Always include `x-lix-primary-key` and `additionalProperties: false` on app schemas.
62
+ - Use version names from the user's vocabulary, such as `"Marketing edit"` or `"Q3 pricing draft"`.
63
+ - Model concurrent-edit domains as collections of small rows because merge is per-row today.
64
+ - Prefer `_by_version` tables for demos, sync, agent inspection, and side-by-side diffs.
65
+ - Close handles in scripts and tests with `await lix.close()`.
66
+
67
+ ## Install And Open
68
+
69
+ ```sh
70
+ npm i @lix-js/sdk better-sqlite3
71
+ ```
36
72
 
37
73
  ```ts
38
74
  import { openLix } from "@lix-js/sdk";
39
75
  import { createBetterSqlite3Backend } from "@lix-js/sdk/sqlite";
40
76
 
41
77
  const lix = await openLix({
42
- backend: createBetterSqlite3Backend({ path: "/path/to/file.lix" }),
78
+ backend: createBetterSqlite3Backend({ path: "/path/to/app.lix" }),
43
79
  });
44
80
  ```
45
81
 
46
- `better-sqlite3` is an optional peer dependency of `@lix-js/sdk`; install it in any project that imports `@lix-js/sdk/sqlite`:
47
-
48
- ```sh
49
- npm i @lix-js/sdk better-sqlite3
50
- ```
51
-
52
- Use the version of this skill that ships with the installed `@lix-js/sdk` package; do not copy version-specific snippets from older releases.
82
+ `better-sqlite3` is an optional peer dependency. Install it in projects that import `@lix-js/sdk/sqlite`.
53
83
 
54
- The default `openLix()` (no `backend`) is in-memory and dies with the process. For anything that needs to persist — demos, scripts, tests, real apps — pass a `better-sqlite3` backend with a real file path. Each successful `execute()` is durable; `lix.close()` releases the backend handle. Reopening with the same path picks up where you left off.
84
+ `openLix()` without a backend is in-memory and dies with the process. For anything that should persist, pass a real `.lix` path. Reopening the same path picks up existing state.
55
85
 
56
- For tests and demos, use an isolated temp dir per run:
86
+ For tests and demos, use an isolated temp directory per run:
57
87
 
58
88
  ```ts
59
89
  import { mkdtempSync } from "node:fs";
60
90
  import { tmpdir } from "node:os";
61
91
  import path from "node:path";
92
+ import { openLix } from "@lix-js/sdk";
93
+ import { createBetterSqlite3Backend } from "@lix-js/sdk/sqlite";
62
94
 
63
95
  const dir = mkdtempSync(path.join(tmpdir(), "lix-"));
64
96
  const lix = await openLix({
@@ -66,36 +98,23 @@ const lix = await openLix({
66
98
  });
67
99
  ```
68
100
 
69
- The handle is intentionally small:
101
+ Use the version of this skill that ships with the installed `@lix-js/sdk` package. If behavior is unclear, inspect the installed package before guessing. The npm package bundles matching engine source under `node_modules/@lix-js/sdk/dist-engine-src/`.
70
102
 
71
- ```ts
72
- type Lix = {
73
- execute(sql: string, params?: readonly unknown[]): Promise<ExecuteResult>;
74
- activeVersionId(): Promise<string>;
75
- createVersion(options: { id?: string; name: string }): Promise<{ versionId: string }>;
76
- switchVersion(options: { versionId: string }): Promise<{ versionId: string }>;
77
- mergeVersion(options: { sourceVersionId: string }): Promise<MergeVersionResult>;
78
- close(): Promise<void>;
79
- };
80
- ```
81
-
82
- Use the public `@lix-js/sdk` API only. Do not import from `engine-wasm`, do not call `initLix` / `createWasmSqliteBackend`, do not open SQLite directly against a `.lix` file.
103
+ Useful installed-package references:
83
104
 
84
- If behavior is unclear, read source before guessing:
105
+ - `dist-engine-src/src/sql2/entity_provider.rs` - registered schema SQL surfaces.
106
+ - `dist-engine-src/src/sql2/change_provider.rs` - `lix_change` projection.
107
+ - `dist-engine-src/src/sql2/version_provider.rs` - writable `lix_version` surface.
108
+ - `dist-engine-src/src/transaction/validation.rs` - primary-key, unique, foreign-key, and shape validation.
109
+ - `dist-engine-src/src/schema/definition.json` - Lix schema-definition meta-schema.
110
+ - `dist-engine-src/src/schema/builtin/` - built-in entity table shapes.
111
+ - `dist-engine-src/src/sql2/udfs/` - registered SQL functions.
85
112
 
86
- - [packages/js-sdk/src/open-lix.ts](https://github.com/opral/lix/blob/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/js-sdk/src/open-lix.ts) current JS API.
87
- - [packages/js-sdk/src/open-lix.test.ts](https://github.com/opral/lix/blob/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/js-sdk/src/open-lix.test.ts) — canonical end-to-end flow.
88
- - [packages/js-sdk/src/sqlite/index.ts](https://github.com/opral/lix/blob/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/js-sdk/src/sqlite/index.ts) — `better-sqlite3` backend factory.
89
- - [packages/engine/src/schema/builtin](https://github.com/opral/lix/tree/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/engine/src/schema/builtin) — built-in entity table shapes.
90
- - [packages/engine/src/sql2/udfs](https://github.com/opral/lix/tree/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/engine/src/sql2/udfs) — registered SQL functions.
113
+ Do not import from `@lix-js/sdk/engine-wasm`, do not call private wasm helpers, and do not open the `.lix` SQLite file directly.
91
114
 
92
- ## Canonical End-To-End Example
115
+ ## Minimal Entity Example
93
116
 
94
- This is the demo to imitate. It shows the four things that make Lix Lix in one script: **isolation** (one SELECT against `<schema>_by_version` returns all versions side by side), **clean parallel merges** (two reviewers editing *different entities* both land on Published), **the audit journal** (`lix_change` is a queryable SQL table), and **conflicts** (two versions edit the *same* entity → `mergeVersion` throws).
95
-
96
- The schema models a brochure as a list of **section entities** (headline, body, disclaimer) — not one row with multiple fields. That matches Lix's per-row merge granularity: Marketing edits the headline section, Legal edits the disclaimer section, both merge cleanly because they touched different rows.
97
-
98
- Note: every registered schema `X` automatically gets a sibling table `X_by_version` exposing a `lixcol_version_id` column. Use it for cross-version reads and writes — you almost never need `switchVersion` for demos or read paths.
117
+ This is the smallest useful consumer pattern: open, register a schema, write a row, read it back, and close.
99
118
 
100
119
  ```ts
101
120
  import { mkdtempSync } from "node:fs";
@@ -108,376 +127,342 @@ const dir = mkdtempSync(path.join(tmpdir(), "lix-"));
108
127
  const lix = await openLix({
109
128
  backend: createBetterSqlite3Backend({ path: path.join(dir, "demo.lix") }),
110
129
  });
111
- const published = await lix.activeVersionId();
112
130
 
113
- // 1. Register a section schema. A brochure is a *collection* of sections.
114
131
  await lix.execute(
115
132
  "INSERT INTO lix_registered_schema (value) VALUES (lix_json($1))",
116
- [JSON.stringify({
117
- $schema: "https://json-schema.org/draft/2020-12/schema",
118
- "x-lix-key": "acme_section",
119
- "x-lix-version": "1",
120
- "x-lix-primary-key": ["/id"],
121
- type: "object",
122
- required: ["id", "brochure_id", "kind", "text"],
123
- properties: {
124
- id: { type: "string" },
125
- brochure_id: { type: "string" },
126
- kind: { type: "string" }, // "headline" | "body" | "disclaimer"
127
- text: { type: "string" },
128
- },
129
- additionalProperties: false,
130
- })],
131
- );
132
-
133
- // 2. Seed three sections of the live brochure on Published.
134
- await lix.execute(
135
- `INSERT INTO acme_section (id, brochure_id, kind, text) VALUES
136
- ($1,$2,$3,$4),($5,$6,$7,$8),($9,$10,$11,$12)`,
137
133
  [
138
- "s-headline", "spring-2026", "headline", "Meet the Acme X1",
139
- "s-body", "spring-2026", "body", "A fast, friendly bike for everyone.",
140
- "s-disclaimer", "spring-2026", "disclaimer", "Specs subject to change.",
134
+ JSON.stringify({
135
+ $schema: "https://json-schema.org/draft/2020-12/schema",
136
+ "x-lix-key": "acme_note",
137
+ "x-lix-version": "1",
138
+ "x-lix-primary-key": ["/id"],
139
+ type: "object",
140
+ required: ["id", "title", "done"],
141
+ properties: {
142
+ id: { type: "string" },
143
+ title: { type: "string" },
144
+ done: { type: "boolean" },
145
+ },
146
+ additionalProperties: false,
147
+ }),
141
148
  ],
142
149
  );
143
150
 
144
- // 3. Create all three reviewer versions up front from the same Published base.
145
- // (Forking later — after a merge — would change the merge base and hide the conflict.)
146
- const marketing = await lix.createVersion({ name: "Marketing edit" });
147
- const legal = await lix.createVersion({ name: "Legal review" });
148
- const competing = await lix.createVersion({ name: "Competing headline" });
149
-
150
- // Marketing rewrites the headline section.
151
- await lix.execute(
152
- `UPDATE acme_section_by_version
153
- SET text = $1
154
- WHERE id = $2 AND lixcol_version_id = $3`,
155
- ["The Acme X1 — built for your weekend", "s-headline", marketing.versionId],
156
- );
157
-
158
- // Legal rewrites the disclaimer section. Different entity → won't conflict with Marketing.
159
151
  await lix.execute(
160
- `UPDATE acme_section_by_version
161
- SET text = $1
162
- WHERE id = $2 AND lixcol_version_id = $3`,
163
- ["Specifications and pricing subject to change. See acme.com/legal.",
164
- "s-disclaimer", legal.versionId],
165
- );
166
-
167
- // Competing rewrites the *same* headline section as Marketing. This will conflict on merge.
168
- await lix.execute(
169
- `UPDATE acme_section_by_version
170
- SET text = $1
171
- WHERE id = $2 AND lixcol_version_id = $3`,
172
- ["Acme X1: now with carbon fork", "s-headline", competing.versionId],
173
- );
174
-
175
- // 4. THE AHA: one SELECT, four versions side by side.
176
- const sideBySide = await lix.execute(
177
- `SELECT v.name, s.kind, s.text
178
- FROM acme_section_by_version s
179
- JOIN lix_version v ON v.id = s.lixcol_version_id
180
- WHERE s.brochure_id = $1
181
- AND s.lixcol_version_id IN ($2, $3, $4, $5)
182
- ORDER BY v.name, s.kind`,
183
- ["spring-2026", published, marketing.versionId, legal.versionId, competing.versionId],
152
+ "INSERT INTO acme_note (id, title, done) VALUES ($1, $2, $3)",
153
+ ["n1", "Draft launch copy", false],
184
154
  );
185
- if (sideBySide.kind === "rows") {
186
- console.log("Same brochure, four versions:");
187
- let lastVersion = "";
188
- for (const row of sideBySide.rows.rows) {
189
- const v = row[0].asText()!;
190
- if (v !== lastVersion) { console.log(` [${v}]`); lastVersion = v; }
191
- console.log(` ${row[1].asText()!.padEnd(11)} → ${row[2].asText()}`);
192
- }
193
- }
194
-
195
- // 5. Merge Marketing and Legal into Published. Different entities → both succeed.
196
- const m1 = await lix.mergeVersion({ sourceVersionId: marketing.versionId });
197
- const m2 = await lix.mergeVersion({ sourceVersionId: legal.versionId });
198
- console.log(`\nMarketing merge: ${m1.outcome} (+${m1.appliedChangeCount} changes)`);
199
- console.log(`Legal merge: ${m2.outcome} (+${m2.appliedChangeCount} changes)`);
200
-
201
- const finalState = await lix.execute(
202
- `SELECT kind, text FROM acme_section
203
- WHERE brochure_id = $1 ORDER BY kind`,
204
- ["spring-2026"],
205
- );
206
- if (finalState.kind === "rows") {
207
- console.log("\nPublished, after both merges (Marketing's headline + Legal's disclaimer):");
208
- for (const row of finalState.rows.rows) {
209
- console.log(` ${row[0].asText()!.padEnd(11)} → ${row[1].asText()}`);
210
- }
211
- }
212
155
 
213
- // 6. The change journal is just a SQL table. Audit log, blame, undo — all queries.
214
- // snapshot_content currently comes back as TEXT (a JSON string), so JSON.parse it.
215
- const history = await lix.execute(
216
- `SELECT created_at, entity_id, snapshot_content
217
- FROM lix_change
218
- WHERE schema_key = $1
219
- ORDER BY created_at`,
220
- ["acme_section"],
156
+ const result = await lix.execute(
157
+ "SELECT title, done FROM acme_note WHERE id = $1",
158
+ ["n1"],
221
159
  );
222
- if (history.kind === "rows") {
223
- console.log(`\nAudit trail (${history.rows.rows.length} entries):`);
224
- for (const row of history.rows.rows) {
225
- const snap = row[2].asText();
226
- const parsed = snap ? JSON.parse(snap) : null;
227
- console.log(` ${row[0].asText()} ${row[1].asText()} ${JSON.stringify(parsed)}`);
228
- }
229
- }
230
160
 
231
- // 7. Try to merge Competing — it edited the same s-headline that Marketing already merged.
232
- // The engine detects the entity-level conflict and throws.
233
- try {
234
- await lix.mergeVersion({ sourceVersionId: competing.versionId });
235
- } catch (err) {
236
- console.log(`\nConflict surfaced (expected): ${(err as Error).message}`);
237
- }
161
+ const row = result.rows[0]!;
162
+ console.log(row.value("title").asText(), row.value("done").asBoolean());
238
163
 
239
164
  await lix.close();
240
165
  ```
241
166
 
242
- What gets printed, in order:
243
-
244
- - One SELECT shows the same brochure across four versions, each section with its own per-version text — versioning lives *in the query layer*.
245
- - Two `mergeCommitted` outcomes with non-zero `appliedChangeCount`.
246
- - A final Published brochure with **Marketing's headline AND Legal's disclaimer** — clean per-row merge.
247
- - An audit trail straight out of `lix_change`, ordered by `created_at`.
248
- - A caught conflict from the version that re-edited Marketing's section.
249
-
250
- That output is the elevator pitch. Imitate this shape when building demos: model the domain as **collections of small entities** (sections, blocks, paragraphs, message keys, line items) so reviewers naturally edit different rows; create all versions up front from the same base; use `<schema>_by_version` for cross-version reads and writes (you almost never need `switchVersion`); name versions in the user's vocabulary (`"Marketing edit"`, `"Legal review"`, `"Editor's pass"`, `"Q3 pricing draft"` — never `"Draft"` or `"branch-1"`); always include a `SELECT FROM lix_change` to surface the audit trail; and a deliberate `try/catch` conflict path.
251
-
252
- ## Cross-Version Reads And Writes
253
-
254
- Every registered schema `X` gets a sibling table `X_by_version` with a `lixcol_version_id` column. Use it whenever a query needs more than one version, or whenever you want to write to a non-active version without `switchVersion`.
167
+ ## Reading Results
255
168
 
256
- - **Reads** filter by `lixcol_version_id` (or omit the filter to scan all versions, joining on `lix_version` for the version name).
257
- - **INSERTs** require `lixcol_version_id` — without it the engine errors with `INSERT into <key>_by_version requires lixcol_version_id`.
258
- - **UPDATEs / DELETEs** must include `lixcol_version_id` in the WHERE clause; DELETEs without it error with `DELETE FROM <key>_by_version requires an explicit lixcol_version_id predicate`.
259
- - The non-suffixed table (`acme_brochure`) is the **active-version view** — convenient for app code that always operates on the current version, but `_by_version` is the right tool for demos, sync, agent inspection, and side-by-side diffs.
169
+ `lix.execute()` returns one shape for every statement:
260
170
 
261
- The same pattern applies to built-ins: `lix_change_by_version`, `lix_commit_by_version`, `lix_directory_by_version`, etc.
171
+ ```ts
172
+ type ExecuteResult = {
173
+ columns: string[];
174
+ rows: Row[];
175
+ rowsAffected: number;
176
+ notices: LixNotice[];
177
+ };
178
+ ```
262
179
 
263
- ## Files (`lix_file`)
180
+ There is no `result.kind`. `SELECT` fills `columns` and `rows`; `INSERT`, `UPDATE`, and `DELETE` usually return `rows: []` and set `rowsAffected`.
264
181
 
265
- Files are first-class in Lix — and this is a core USP, not a side feature. The built-in `lix_file` table lets you write any file (text, JSON, markdown, binary) into the lix and get versioning, branching, merging, and history over it for free. Parent directories are created automatically.
182
+ Each row is a `Row` object. Use `row.value("column")` or `row.valueAt(index)` to get a `Value`, then call typed accessors:
266
183
 
267
184
  ```ts
268
- // Write a file. `data` is a blob (bytes).
269
- await lix.execute(
270
- "INSERT INTO lix_file (id, path, data) VALUES ($1, $2, $3)",
271
- ["file-readme", "/docs/readme.md", new TextEncoder().encode("# Hello\n")],
272
- );
273
-
274
- // Read it back.
275
- const r = await lix.execute(
276
- "SELECT path, data FROM lix_file WHERE id = $1",
277
- ["file-readme"],
278
- );
279
- if (r.kind === "rows") {
280
- const [path, data] = r.rows.rows[0]!;
281
- console.log(path.asText(), new TextDecoder().decode(data.asBlob()!));
185
+ const r = await lix.execute("SELECT id, title, done FROM acme_note");
186
+ for (const row of r.rows) {
187
+ const id = row.value("id").asText();
188
+ const title = row.value("title").asText();
189
+ const done = row.value("done").asBoolean();
282
190
  }
283
191
  ```
284
192
 
285
- Columns on `lix_file`:
193
+ | Method | Returns | Use for |
194
+ | ------------- | ------------------------- | ----------------------------------------- |
195
+ | `asText()` | `string \| undefined` | strings; note `asText`, not `asString` |
196
+ | `asBoolean()` | `boolean \| undefined` | booleans |
197
+ | `asInteger()` | `number \| undefined` | integer fields |
198
+ | `asReal()` | `number \| undefined` | decimal/real fields |
199
+ | `asJson()` | `JsonValue \| undefined` | objects and arrays |
200
+ | `asBlob()` | `Uint8Array \| undefined` | binary data |
286
201
 
287
- | Column | Type | What it is |
288
- |--------|------|------------|
289
- | `id` | text | Stable identity for the file (you choose it). |
290
- | `path` | text | Absolute path like `/docs/readme.md`. Parent directories are auto-created in `lix_directory`. |
291
- | `data` | blob | File contents as bytes. |
292
- | `hidden` | bool | UI hint; doesn't affect storage. |
293
- | `lixcol_*` | various | Version metadata (`lixcol_version_id`, `lixcol_global`, `lixcol_untracked`, `lixcol_change_id`, `lixcol_commit_id`, ...). |
202
+ Accessors return `undefined` when the cell kind does not match. Branch on `value.kind` if a column can hold multiple types. Public kind strings are `"null"`, `"boolean"`, `"integer"`, `"real"`, `"text"`, `"json"`, and `"blob"`.
294
203
 
295
- `lix_file_by_version` exists for cross-version file reads/writes, exactly like any other entity surface.
296
-
297
- **Files-as-entities (upcoming).** A future plugin API will let a registered parser turn a file's contents into rows in your schema tables on write — so `messages.json` becomes per-key entities and two translators editing different keys merge cleanly. Not shipped through `@lix-js/sdk` yet; don't promise it in demos. Today, `lix_file` versions bytes only.
204
+ `Row` also has convenience methods when native JS values are enough: `get(name)`, `tryGet(name)`, `getAt(index)`, `toObject()`, and `toValueMap()`.
298
205
 
299
206
  ## Registering Schemas
300
207
 
301
- Use stable, namespaced `x-lix-key` and `x-lix-version`. Prefer names that describe a domain entity (`acme_brochure`, `crm_contact`, `cms_page`) — never generic ones (`task`, `item`).
208
+ Register app schemas by inserting JSON into `lix_registered_schema.value`:
302
209
 
303
- Include `x-lix-primary-key` so the engine can derive entity identity. Each entry is a **JSON Pointer (RFC 6901)** into the entity, leading slash required:
210
+ ```ts
211
+ await lix.execute(
212
+ "INSERT INTO lix_registered_schema (value) VALUES (lix_json($1))",
213
+ [JSON.stringify(schema)],
214
+ );
215
+ ```
304
216
 
305
- - `["/id"]` — top-level `id` property.
306
- - `["/owner/email"]` — nested property `owner.email`.
307
- - `["/owner", "/slug"]` — composite key over two top-level fields.
217
+ Schema basics:
308
218
 
309
- Without `x-lix-primary-key`, table-style INSERTs fail with `requires lixcol_entity_id because the schema has no x-lix-primary-key`.
219
+ - `x-lix-key` becomes the generated SQL table name.
220
+ - `x-lix-version` versions your schema contract.
221
+ - `x-lix-primary-key` tells Lix how to derive entity identity.
222
+ - Primary-key entries are JSON Pointers with a leading slash, such as `["/id"]` or `["/owner/email"]`.
223
+ - Use `additionalProperties: false` so accidental fields fail fast.
310
224
 
311
- ## Reading Results
225
+ Without `x-lix-primary-key`, table-style INSERTs fail with an error like `requires lixcol_entity_id because the schema has no x-lix-primary-key`.
312
226
 
313
- `lix.execute()` returns a discriminated `ExecuteResult`:
227
+ Uniqueness is not inferred from ordinary JSON Schema fields. If a non-primary-key field must be unique, declare it explicitly:
314
228
 
315
229
  ```ts
316
- type ExecuteResult =
317
- | { kind: "rows"; rows: { columns: string[]; rows: Value[][] } }
318
- | { kind: "affectedRows"; affectedRows: number };
230
+ const companyDomainSchema = {
231
+ "x-lix-key": "crm_company_domain",
232
+ "x-lix-version": "1",
233
+ "x-lix-primary-key": ["/id"],
234
+ "x-lix-unique": [["/domain"]],
235
+ type: "object",
236
+ required: ["id", "domain"],
237
+ properties: {
238
+ id: { type: "string" },
239
+ domain: { type: "string" },
240
+ },
241
+ additionalProperties: false,
242
+ };
319
243
  ```
320
244
 
321
- `SELECT` returns `kind: "rows"`. `INSERT` / `UPDATE` / `DELETE` return `kind: "affectedRows"`. Always narrow on `result.kind` before reading `result.rows`.
245
+ Do not add generic `created_at` or `updated_at` fields by default. Lix already records lifecycle history through `lix_change` and `lixcol_*` metadata. Add timestamp fields only when they are domain data, such as `due_at`, `published_at`, or `occurred_at`.
322
246
 
323
- Cells in `result.rows.rows[i][j]` are `Value` instances (also exported from `@lix-js/sdk`), not raw JS primitives. Use the typed accessors:
247
+ Discover live schemas before guessing:
324
248
 
325
249
  ```ts
326
- import { openLix, Value } from "@lix-js/sdk";
327
-
328
- const r = await lix.execute("SELECT id, headline, price_usd FROM acme_brochure");
329
- if (r.kind === "rows") {
330
- for (const row of r.rows.rows) {
331
- const id = row[0].asText(); // string | undefined
332
- const headline = row[1].asText(); // string | undefined
333
- const price = row[2].asReal(); // number | undefined
334
- }
250
+ const schemas = await lix.execute(
251
+ "SELECT lixcol_entity_id, value FROM lix_registered_schema ORDER BY lixcol_entity_id",
252
+ );
253
+
254
+ for (const row of schemas.rows) {
255
+ const schema = row.get("value") as { "x-lix-key"?: string };
256
+ console.log(schema["x-lix-key"]);
335
257
  }
336
258
  ```
337
259
 
338
- | Method | Returns | Use for |
339
- |--------|---------|---------|
340
- | `asText()` | `string \| undefined` | `string` (note: `asText`, not `asString`) |
341
- | `asBoolean()` | `boolean \| undefined` | `boolean` |
342
- | `asInteger()` | `number \| undefined` | `integer` |
343
- | `asReal()` | `number \| undefined` | `number` |
344
- | `asJson()` | `JsonValue \| undefined` | `object` / `array` |
345
- | `asBlob()` | `Uint8Array \| undefined` | binary |
346
- | `kindValue()` | `"text" \| "bool" \| "int" \| "float" \| "json" \| "blob" \| "null"` | discriminator |
260
+ ## Versions And `_by_version`
347
261
 
348
- Each accessor returns `undefined` if the cell's kind doesn't match branch on `kindValue()` first if you need to handle multiple types. Note the naming mismatch: accessors are `asInteger` / `asReal`, but `kindValue()` returns the short forms `"int"` / `"float"`.
262
+ Capture the initial active version id instead of hardcoding `"main"`:
349
263
 
350
- ## Versions
264
+ ```ts
265
+ const published = await lix.activeVersionId();
266
+ ```
351
267
 
352
- Capture the initial active version id rather than hardcoding `"main"`:
268
+ Create versions with names from the user's domain:
353
269
 
354
270
  ```ts
355
- const published = await lix.activeVersionId();
271
+ const marketing = await lix.createVersion({ name: "Marketing edit" });
272
+ const legal = await lix.createVersion({ name: "Legal review" });
356
273
  ```
357
274
 
358
- Create a version with a name from the user's vocabulary:
275
+ Every registered schema `X` gets a sibling table `X_by_version` with `lixcol_version_id`. Use it for side-by-side reads and for writes to non-active versions.
359
276
 
360
277
  ```ts
361
- const draft = await lix.createVersion({ name: "Marketing edit" });
278
+ await lix.execute(
279
+ `UPDATE acme_note_by_version
280
+ SET title = $1
281
+ WHERE id = $2 AND lixcol_version_id = $3`,
282
+ ["Sharper launch copy", "n1", marketing.id],
283
+ );
284
+
285
+ const sideBySide = await lix.execute(
286
+ `SELECT v.name, n.title
287
+ FROM acme_note_by_version n
288
+ JOIN lix_version v ON v.id = n.lixcol_version_id
289
+ WHERE n.id = $1
290
+ AND n.lixcol_version_id IN ($2, $3)
291
+ ORDER BY v.name`,
292
+ ["n1", published, marketing.id],
293
+ );
362
294
  ```
363
295
 
364
- For demos, agent flows, sync, and any read path that touches more than one version, **write through `<schema>_by_version` with `lixcol_version_id`** (see the canonical demo) — you don't need to switch.
296
+ Rules for `_by_version`:
297
+
298
+ - Reads filter by `lixcol_version_id`, or omit the filter to scan all versions.
299
+ - INSERTs require `lixcol_version_id`.
300
+ - UPDATEs and DELETEs must include `lixcol_version_id` in the WHERE clause.
301
+ - The non-suffixed table is the active-version view.
365
302
 
366
- `switchVersion` is for app code that has a "current working version" concept and wants subsequent unqualified writes (`UPDATE acme_section SET …`) to land there. `mergeVersion` always merges *into the active version*, so if you're merging into something other than the currently active version, switch first.
303
+ `switchVersion()` is for app code with a current working version concept. `mergeVersion()` always merges into the active version, so switch first if you need a different target.
367
304
 
368
305
  ## Merging
369
306
 
370
- `mergeVersion()` merges the source version into the **currently active** version (no `targetVersionId`):
307
+ `mergeVersion()` merges the source version into the currently active version:
371
308
 
372
309
  ```ts
373
- const merge = await lix.mergeVersion({ sourceVersionId: draft.versionId });
310
+ try {
311
+ const merge = await lix.mergeVersion({ sourceVersionId: marketing.id });
312
+ console.log(merge.outcome, merge.changeStats.total);
313
+ } catch (error) {
314
+ console.error("Merge conflict", error);
315
+ }
374
316
  ```
375
317
 
376
- The result is a structured receipt:
318
+ Common outcomes:
377
319
 
378
- ```ts
379
- type MergeVersionResult = {
380
- outcome: "alreadyUpToDate" | "mergeCommitted";
381
- appliedChangeCount: number;
382
- targetVersionId: string;
383
- sourceVersionId: string;
384
- mergeBaseCommitId: string | null;
385
- targetHeadBeforeCommitId: string;
386
- sourceHeadBeforeCommitId: string;
387
- targetHeadAfterCommitId: string;
388
- createdMergeCommitId: string | null;
389
- };
390
- ```
320
+ - `"alreadyUpToDate"` - source has no commits the target lacks.
321
+ - `"fastForward"` - target advanced to source without a merge commit.
322
+ - `"mergeCommitted"` - a new merge commit was created.
323
+
324
+ `mergeVersionPreview()` reports the same merge decision without advancing refs, staging changes, or creating commits. Merge conflicts are returned as preview data.
325
+
326
+ Conflicts throw from `mergeVersion()`. If both versions modified the same entity since their merge base, Lix raises a `LixError`. Conflict detection is row-level today, not field-level. To reproduce a conflict in a demo, fork all contending versions from the same base before merging any of them.
327
+
328
+ ## Demo Pattern To Imitate
391
329
 
392
- - `outcome: "alreadyUpToDate"` the source has no commits the target lacks (including self-merge). Nothing applied.
393
- - `outcome: "mergeCommitted"` — a new merge commit was created; `appliedChangeCount > 0` and `createdMergeCommitId` is set.
330
+ For richer demos, show these four things:
394
331
 
395
- **Conflicts throw.** If both versions modified the same entity (the same row, identified by `x-lix-key` + primary key) since their merge base, `mergeVersion` raises a `LixError` with a message like `engine2 merge_version found N tracked-state conflict(s)`. Conflicts are detected at row identity, not at field level — disjoint-field edits to the same row still conflict. The current SDK does not expose programmatic conflict resolution — wrap in `try/catch` and surface the error to the user (see the canonical demo above).
332
+ 1. Isolation: one SELECT against `<schema>_by_version` shows several versions side by side.
333
+ 2. Clean parallel merges: two reviewers edit different entities and both land.
334
+ 3. Audit history: `lix_change` is queryable SQL.
335
+ 4. Conflict handling: two versions edit the same entity and `mergeVersion()` throws.
396
336
 
397
- ## SQL Parameters and UDFs
337
+ Shape the domain as a collection of small entities:
398
338
 
399
- Use DataFusion-style numbered placeholders. Bare `?` is rejected with `Failed to parse placeholder id`:
339
+ - Good: brochure sections, document blocks, paragraph rows, message keys, line items.
340
+ - Risky: one huge document row with many editable fields.
341
+
342
+ Demo recipe:
343
+
344
+ 1. Register a schema such as `acme_section`.
345
+ 2. Seed several rows in the published version.
346
+ 3. Create all reviewer versions up front from the same base.
347
+ 4. Write each reviewer's changes through `acme_section_by_version`.
348
+ 5. Read side by side by joining `acme_section_by_version` to `lix_version`.
349
+ 6. Merge non-overlapping row edits successfully.
350
+ 7. Query `lix_change`.
351
+ 8. Catch the deliberate same-row conflict.
352
+
353
+ ## Files With `lix_file`
354
+
355
+ `lix_file` stores files as versioned bytes. Parent directories are created automatically.
400
356
 
401
357
  ```ts
402
- await lix.execute("SELECT * FROM acme_brochure WHERE id = $1", ["spring-2026"]);
403
- ```
358
+ await lix.execute("INSERT INTO lix_file (id, path, data) VALUES ($1, $2, $3)", [
359
+ "file-readme",
360
+ "/docs/readme.md",
361
+ new TextEncoder().encode("# Hello\n"),
362
+ ]);
404
363
 
405
- The only UDF the canonical demo uses is `lix_json($1)` — it parses a TEXT parameter as a JSON value, required when writing JSON-typed columns like `lix_registered_schema.value`. Other UDFs (`lix_json_get`, `lix_json_get_text`, `lix_uuid_v7`, `lix_text_encode`/`_decode`, `lix_empty_blob`, …) live in [packages/engine/src/sql2/udfs](https://github.com/opral/lix/tree/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/engine/src/sql2/udfs) — read source when you need them.
364
+ const result = await lix.execute(
365
+ "SELECT path, data FROM lix_file WHERE id = $1",
366
+ ["file-readme"],
367
+ );
406
368
 
407
- ## Built-in Queryable Tables
369
+ const file = result.rows[0]!;
370
+ console.log(
371
+ file.value("path").asText(),
372
+ new TextDecoder().decode(file.value("data").asBlob()!),
373
+ );
374
+ ```
408
375
 
409
- The four tables demos actually touch:
376
+ Columns consumers usually need:
410
377
 
411
- | Table | What it gives you |
412
- |-------|-------------------|
413
- | `lix_version` | List of versions (`id`, `name`, `hidden`, `commit_id`). Use this instead of hardcoding `"main"`. |
414
- | `lix_change` | The immutable journal — every change with `entity_id`, `schema_key`, `snapshot_content`, `created_at`. See the next section. |
415
- | `lix_file` | Built-in file storage (covered above). |
416
- | `lix_registered_schema` | Your registered schemas (and built-ins). |
378
+ | Column | What it is |
379
+ | ---------- | --------------------------------------------------------------------- |
380
+ | `id` | Stable identity for the file. |
381
+ | `path` | Absolute path like `/docs/readme.md`. |
382
+ | `data` | File contents as bytes. |
383
+ | `hidden` | UI hint; does not affect storage. |
384
+ | `lixcol_*` | Version/change metadata, including `lixcol_version_id` where exposed. |
417
385
 
418
- The engine ships ~20 more built-ins (commit graph, change sets, key-value, labels, file/directory descriptors, low-level state, etc.) — see [packages/engine/src/schema/builtin](https://github.com/opral/lix/tree/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/engine/src/schema/builtin) when you need them. `lix_account` / `lix_change_author` are declared but **not yet implemented; don't rely on them.**
386
+ `lix_file_by_version` exists for cross-version file reads and writes. Files-as-parsed-entities are product direction, not current JS SDK behavior.
419
387
 
420
- ## The Change Journal (`lix_change`)
388
+ ## The Change Journal
421
389
 
422
- `lix_change` is the most reliable feature in the SDK and the most under-marketed one. Every write — INSERT, UPDATE, DELETE, on any registered schema, on any version — appends an immutable row. That row is **just SQL you can query**. Audit logs, blame, undo, "what changed since Tuesday", "show me everything Marketing edited" — none of this is a separate subsystem; it's all `SELECT FROM lix_change`.
390
+ `lix_change` is an immutable SQL table of changes across registered schemas and versions. Use it for audit logs, blame, history, activity feeds, and undo-style UI.
423
391
 
424
- Columns that matter: `id`, `entity_id`, `schema_key`, `schema_version`, `snapshot_content` (TEXT JSON — `JSON.parse(asText())`, not `asJson()`), `created_at`, plus `lixcol_*` for version metadata.
392
+ Important columns include `id`, `entity_id`, `schema_key`, `schema_version`, `snapshot_content`, `created_at`, and `lixcol_*` metadata.
425
393
 
426
394
  ```ts
427
- // Audit log: every change to a single entity, oldest to newest.
395
+ // Audit log for one entity, oldest to newest.
428
396
  await lix.execute(
429
397
  `SELECT created_at, snapshot_content
430
398
  FROM lix_change
431
399
  WHERE schema_key = $1 AND entity_id = $2
432
400
  ORDER BY created_at`,
433
- ["acme_section", "s-headline"],
401
+ ["acme_note", "n1"],
434
402
  );
435
403
 
436
- // Blame: who/what last touched this entity.
437
- await lix.execute(
438
- `SELECT created_at, snapshot_content
439
- FROM lix_change
440
- WHERE schema_key = $1 AND entity_id = $2
441
- ORDER BY created_at DESC
442
- LIMIT 1`,
443
- ["acme_section", "s-headline"],
444
- );
445
-
446
- // Activity feed: latest 20 changes across the whole schema.
404
+ // Latest activity across a schema.
447
405
  await lix.execute(
448
406
  `SELECT created_at, entity_id, snapshot_content
449
407
  FROM lix_change
450
408
  WHERE schema_key = $1
451
409
  ORDER BY created_at DESC
452
410
  LIMIT 20`,
453
- ["acme_section"],
411
+ ["acme_note"],
454
412
  );
455
413
  ```
456
414
 
457
- Reach for `lix_change` whenever an app needs history, audit, undo, or activity feeds. It replaces a hand-rolled audit table.
415
+ `snapshot_content` can be null or absent for tombstones, removals, or rows where content was not materialized. In the JS SDK, read it with `row.value("snapshot_content").asJson()` or `row.get("snapshot_content")`, then handle null. Do not blindly `JSON.parse` it as text.
416
+
417
+ ## Built-In Tables And UDFs
458
418
 
459
- ## Gotchas
419
+ Common tables:
460
420
 
461
- A short list of things that will burn a fresh agent on the first run.
421
+ | Table | What it gives consumers |
422
+ | ----------------------- | ------------------------------------------------------------------------------------------------------- |
423
+ | `lix_version` | Writable version surface: `id`, `name`, `hidden`, `commit_id`. |
424
+ | `lix_change` | Immutable change journal. |
425
+ | `lix_file` | Versioned byte storage for files. |
426
+ | `lix_registered_schema` | Registry of app schemas plus built-ins; also exposes the Lix schema-definition meta-schema at runtime. |
462
427
 
463
- - **`type: "number"` columns require float literals.** JS numerics bind as Int64 by default, so seeding a `number` column with `899` fails with `expected Float64 but found Int64`. JS has no integer/float distinction, so `899.0` doesn't help either — pass a value with a fractional part (e.g. `899.5`), or model whole-number fields as `type: "integer"` and use `asInteger()` to read.
464
- - **`lix_change.snapshot_content` is currently a TEXT column** (a JSON string), not native JSON. Reading it with `Value.asJson()` returns `undefined`. Use `JSON.parse(cell.asText())` instead.
465
- - **Merge is per-entity, not per-field.** Two versions modifying the same row conflict even if their fields are disjoint. Model concurrent-edit domains as collections of small entities (see the canonical demo).
466
- - **Conflict reproduction is order-dependent.** Fork *all* contending versions from the same base before any merge happens. A version forked *after* a merge has the merged state as its base, so its edit becomes a clean fast-forward instead of a conflict.
467
- - **`mergeVersion` always merges into the active version.** There is no `targetVersionId` parameter. If you need a non-active target, `switchVersion` first.
468
- - **Bare `?` placeholders are rejected** with `Failed to parse placeholder id`. Always use `$1`, `$2`, ...
428
+ `lix_version` can be updated for admin flows:
429
+
430
+ ```ts
431
+ await lix.execute("UPDATE lix_version SET hidden = true WHERE id = $1", [
432
+ marketing.id,
433
+ ]);
434
+ ```
469
435
 
470
- ## Safety Rules
436
+ There is no documented `deleteVersion()` helper in this preview. If the product wants reversible cleanup, hide the version. If it wants removal, `DELETE FROM lix_version WHERE id = $1` is the SQL surface; the engine rejects deleting the global version and active version.
471
437
 
472
- - Never use `sqlite3`, `sql.js`, or raw database access against a `.lix`; use `createBetterSqlite3Backend()` instead of opening the file yourself.
473
- - Never import from `@lix-js/sdk/engine-wasm` or other private internals.
474
- - Use `$1`, `$2`, `$3` placeholders, never bare `?`.
475
- - Schema keys: stable, namespaced, lowercase-snake-case, domain-shaped (`acme_brochure`, not `task`).
476
- - Always include `x-lix-primary-key` and `additionalProperties: false` on registered schemas.
477
- - Name versions in the end-user's vocabulary (`"Marketing edit"`, `"Q3 pricing draft"`), never developer jargon (`"Draft"`, `"branch-1"`).
478
- - Wrap `mergeVersion` in `try/catch` if there is any chance of a conflicting edit.
479
- - Close handles in scripts/tests with `await lix.close()`.
438
+ Use `lix_json($1)` to parse JSON text parameters when writing JSON-typed columns:
439
+
440
+ ```ts
441
+ await lix.execute(
442
+ "INSERT INTO lix_registered_schema (value) VALUES (lix_json($1))",
443
+ [JSON.stringify(schema)],
444
+ );
445
+ ```
446
+
447
+ Other UDFs, such as `lix_json_get`, `lix_uuid_v7`, `lix_text_encode`, and `lix_empty_blob`, live in `dist-engine-src/src/sql2/udfs/` in the installed package.
448
+
449
+ ## Do And Avoid
450
+
451
+ | Do | Avoid |
452
+ | --- | --- |
453
+ | Use `createBetterSqlite3Backend({ path })` for persistent state. | Opening `.lix` files with raw SQLite libraries. |
454
+ | Use public imports from `@lix-js/sdk` and `@lix-js/sdk/sqlite`. | Importing `engine-wasm` or private internals. |
455
+ | Use `$1`, `$2`, `$3` placeholders. | Bare `?` placeholders. |
456
+ | Use `lix_json($1)` for JSON parameters. | Inlining stringified JSON directly into SQL. |
457
+ | Use `_by_version` for cross-version reads/writes. | Switching versions just to render a side-by-side view. |
458
+ | Name versions in user vocabulary. | User-facing words like branch, branch-1, or generic Draft. |
459
+ | Model collaborative data as small rows. | One giant row when multiple reviewers edit different parts. |
460
+ | Add `x-lix-unique` for non-primary unique fields. | Assuming JSON Schema property metadata creates uniqueness. |
461
+ | Read `snapshot_content` as JSON/native and handle null. | Blindly `JSON.parse(row.value(...).asText())`. |
462
+ | Wrap `mergeVersion()` in `try/catch`. | Assuming merges cannot conflict. |
480
463
 
481
464
  ## Reporting SDK Friction
482
465
 
483
- If you encounter an SDK bug, missing API, confusing error, documentation gap, or large implementation friction while using this skill, pause and ask the user whether they want you to open a GitHub issue via the `gh` CLI installed on their computer. Do not file the issue without confirmation. If they approve, include a minimal reproduction, expected behavior, actual behavior, the installed `@lix-js/sdk` package version, runtime/version details, and any relevant error output.
466
+ If you encounter an SDK bug, missing API, confusing error, documentation gap, or large implementation friction while using this skill, pause and ask the user whether they want you to open a GitHub issue via the `gh` CLI installed on their computer. Do not file an issue without confirmation.
467
+
468
+ Before filing, scan existing issues to avoid duplicates. If the user approves a report, include a minimal reproduction, expected behavior, actual behavior, the installed `@lix-js/sdk` version, runtime details, and relevant error output. Do not include private data, customer content, credentials, tokens, local paths, database contents, or proprietary schemas.