@abloatai/ablo 0.6.0 → 0.7.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 (74) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +64 -35
  3. package/dist/BaseSyncedStore.d.ts +1 -1
  4. package/dist/BaseSyncedStore.js +1 -1
  5. package/dist/client/Ablo.d.ts +1 -0
  6. package/dist/client/Ablo.js +1 -0
  7. package/dist/client/createModelProxy.d.ts +26 -3
  8. package/dist/client/createModelProxy.js +4 -1
  9. package/dist/client/validateAbloOptions.js +2 -2
  10. package/dist/coordination/index.d.ts +6 -0
  11. package/dist/coordination/index.js +6 -0
  12. package/dist/coordination/schema.d.ts +329 -0
  13. package/dist/coordination/schema.js +209 -0
  14. package/dist/core/QueryView.d.ts +4 -1
  15. package/dist/core/QueryView.js +1 -1
  16. package/dist/core/query-utils.d.ts +7 -10
  17. package/dist/core/query-utils.js +2 -3
  18. package/dist/errorCodes.d.ts +264 -0
  19. package/dist/errorCodes.js +251 -0
  20. package/dist/errors.d.ts +51 -6
  21. package/dist/errors.js +56 -3
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +2 -2
  24. package/dist/policy/index.d.ts +1 -1
  25. package/dist/policy/index.js +1 -1
  26. package/dist/policy/types.d.ts +31 -0
  27. package/dist/policy/types.js +15 -0
  28. package/dist/react/AbloProvider.d.ts +12 -0
  29. package/dist/react/AbloProvider.js +11 -3
  30. package/dist/schema/ddl.d.ts +62 -0
  31. package/dist/schema/ddl.js +317 -0
  32. package/dist/schema/diff.d.ts +6 -0
  33. package/dist/schema/diff.js +21 -3
  34. package/dist/schema/field.d.ts +16 -19
  35. package/dist/schema/field.js +30 -17
  36. package/dist/schema/index.d.ts +7 -4
  37. package/dist/schema/index.js +9 -3
  38. package/dist/schema/model.d.ts +87 -25
  39. package/dist/schema/model.js +33 -3
  40. package/dist/schema/relation.d.ts +17 -0
  41. package/dist/schema/roles.d.ts +148 -0
  42. package/dist/schema/roles.js +149 -0
  43. package/dist/schema/schema.d.ts +2 -112
  44. package/dist/schema/schema.js +50 -62
  45. package/dist/schema/select.d.ts +25 -0
  46. package/dist/schema/select.js +55 -0
  47. package/dist/schema/serialize.d.ts +13 -9
  48. package/dist/schema/serialize.js +14 -10
  49. package/dist/schema/sugar.d.ts +20 -3
  50. package/dist/schema/sugar.js +5 -1
  51. package/dist/schema/tenancy.d.ts +66 -0
  52. package/dist/schema/tenancy.js +58 -0
  53. package/dist/sync/HydrationCoordinator.d.ts +2 -0
  54. package/dist/sync/HydrationCoordinator.js +23 -17
  55. package/dist/sync/createIntentStream.d.ts +2 -1
  56. package/dist/sync/createIntentStream.js +46 -1
  57. package/dist/sync/participants.js +5 -14
  58. package/dist/types/streams.d.ts +53 -33
  59. package/docs/api-keys.md +44 -0
  60. package/docs/api.md +11 -22
  61. package/docs/cli.md +212 -0
  62. package/docs/client-behavior.md +1 -1
  63. package/docs/coordination.md +61 -12
  64. package/docs/data-sources.md +2 -2
  65. package/docs/examples/existing-python-backend.md +3 -3
  66. package/docs/examples/scoped-agent.md +78 -0
  67. package/docs/guarantees.md +5 -2
  68. package/docs/identity.md +139 -68
  69. package/docs/index.md +6 -0
  70. package/docs/integration-guide.md +31 -35
  71. package/docs/interaction-model.md +3 -0
  72. package/docs/react.md +3 -3
  73. package/docs/roadmap.md +14 -2
  74. package/package.json +8 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Structured error contract, schema/migration engine, and a full `ablo` CLI.
8
+ - **Structured error contract across HTTP + WS planes.** A closed, canonical
9
+ error-code registry is now the `code` tier of a Stripe-style error model. A
10
+ single HTTP egress funnel converts every throw to a canonical
11
+ `{ type, code, message, doc_url, request_id, ...details }` envelope; the WS
12
+ plane narrows mutation/claim error codes to the same union.
13
+ - **Versioned contract + drift guard.** `ERROR_CONTRACT_VERSION` (date-based)
14
+ ships in `errors.json` and on the `Ablo-Version` response header, so consumers
15
+ detect contract changes without diffing docs. Generated `errors.mdx` /
16
+ `errors.json` plus a CI drift guard keep the docs, OpenAPI spec, and SDK from
17
+ silently diverging from the registry.
18
+ - **Always-on request correlation.** Every response carries a `req_…` request id
19
+ (honoring an inbound `x-request-id`), stamped into the envelope's `request_id`.
20
+ - **OpenAPI parity.** The stale `{ error, reason }` schema is replaced by the
21
+ canonical envelope plus a generated `ErrorCode` enum.
22
+
23
+ CLI + schema:
24
+ - **Schema diff + migration planning engine** (`generateProvisionPlan` /
25
+ `generateMigrationPlan` in `@abloatai/ablo/schema`) — pure diff, classify,
26
+ apply, and constant-value backfill for required-field migrations.
27
+ - **`ablo generate`** — emit TypeScript types from the pushed schema.
28
+ - **Full `ablo` CLI suite**, Stripe-CLI-shaped: `init`, `login` / `logout` /
29
+ `status`, `mode [test|live]`, `dev` (push schema to the test sandbox + watch),
30
+ `logs` (tail your scope's commit activity), and the data-source commands below.
31
+ Authentication is the OAuth 2.0 device flow; `login` provisions and stores a
32
+ test and a live key, and `mode` switches the active one.
33
+ - **Database-URL structure (bring-your-own-database).** The CLI is split by where
34
+ it writes:
35
+ - `ablo pull` / `ablo check` / `ablo migrate` operate on **your own
36
+ `DATABASE_URL`** — `pull` introspects it to emit `defineSchema(...)` from
37
+ existing tables (read-only, like `prisma db pull`), `check` verifies tables
38
+ fit the schema with no DDL, and `migrate` applies DDL to `DATABASE_URL`.
39
+ - `ablo schema push` / `ablo dev` target the **hosted** test/live sandbox; the
40
+ server diffs, migrates, and activates the uploaded schema. `dev` never
41
+ touches live data.
42
+
43
+ **BREAKING** — removed the legacy React hooks `useQuery` / `useOne` / `useMutate`
44
+ / `useReader`. Use `useAblo()` + `ablo.<model>.*` instead. The `MutateActions`,
45
+ `ReaderActions`, and `ReaderFindOptions` types are still re-exported for callers
46
+ that referenced them.
47
+
3
48
  ## 0.6.0
4
49
 
5
50
  ### Minor Changes
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Ablo
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@abloatai/ablo.svg)](https://www.npmjs.com/package/@abloatai/ablo)
4
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
5
+ [![types](https://img.shields.io/badge/types-included-blue.svg)](#)
6
+ [![runtime](https://img.shields.io/badge/node-%E2%89%A522-brightgreen.svg)](#keys--runtime)
7
+
3
8
  Ablo is a typed sync engine for shared app state — the kind that humans,
4
9
  server code, and AI agents all edit at once.
5
10
 
@@ -11,29 +16,46 @@ of who changed what.
11
16
  schema -> ablo.<model>.create/retrieve/update/claim(...)
12
17
  ```
13
18
 
19
+ ## Why Ablo
20
+
21
+ - **Real-time by default.** Every `create` / `update` / `delete` fans out
22
+ confirmed deltas to all subscribers — humans and agents — with no separate
23
+ "multiplayer mode" to switch on.
24
+ - **No silent clobbers.** Writes are guarded against stale reads, and `claim`
25
+ holds a row across a slow read → LLM → write gap so concurrent edits queue
26
+ instead of overwriting.
27
+ - **Built for agents.** See who's mid-edit (`claimState` / `queue`), coordinate a
28
+ fair line, and ship an `llms.txt` so coding agents integrate from the real API.
29
+ - **Typed end to end.** Your Zod schema produces typed model proxies
30
+ (`ablo.<model>.update(...)`), optimistic local reads, and reactive React hooks.
31
+ - **Bring your own auth and database.** Ablo scopes realtime data to *sync
32
+ groups* from your existing identity, and can leave your database as the source
33
+ of truth via a Data Source.
34
+
35
+ **Built for:** collaborative editors, AI agent workflows, internal tools, and any
36
+ app where multiple actors mutate shared state and everyone must see it live.
37
+
14
38
  ## Set up
15
39
 
16
40
  ```bash
17
41
  npm install @abloatai/ablo
18
42
  ```
19
43
 
20
- The package ships an `llms.txt` a precise map of the API — so a coding agent
21
- integrates from the real surface instead of guessing it. Point Claude Code or
22
- Cursor at it:
23
-
24
- > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema and
25
- > `<AbloProvider>`, wire my first create / retrieve / update, and use `claim`
26
- > for anything an agent edits across a slow step (read → LLM → write).
44
+ **Keys & runtime.** Ablo needs Node 22+ and TypeScript 5+. Grab an `sk_test_*`
45
+ key for a sandbox
46
+ (`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server runtimes only.
47
+ In the browser, `<AbloProvider>` authenticates with the signed-in user's
48
+ session never the raw key.
27
49
 
28
- Or wire it by hand — the [Quick Start](#quick-start) below is the shape it
29
- produces. For production (React, an existing backend, Data Source, agents), the
50
+ Then wire it by hand — the [Quick Start](#quick-start) below is the shape to
51
+ copy. For production (React, an existing backend, Data Source, agents), the
30
52
  [Integration Guide](./docs/integration-guide.md) is the deeper map.
31
53
 
32
- **Keys & runtime.** Ablo is ESM-only (`import`, not `require`) and needs Node
33
- 22+ and TypeScript 5+. Grab an `sk_test_*` key
34
- for a sandbox (`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server
35
- runtimes only. In the browser, `<AbloProvider>` authenticates with the signed-in
36
- user's session never the raw key.
54
+ **Prefer to let an agent wire it?** The package ships an `llms.txt` a precise
55
+ map of the API so Claude Code or Cursor integrates from the real surface
56
+ instead of guessing:
57
+
58
+ > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema, a `<AbloProvider>`, and my first create / retrieve / update.
37
59
 
38
60
  ## Quick Start
39
61
 
@@ -87,23 +109,23 @@ reactive under `useAblo`/`subscribe`. `load(...)` fetches from the server when a
87
109
  row may not be local yet.
88
110
 
89
111
  ```ts
90
- ablo.weatherReports.retrieve('report_stockholm'); // → row | undefined
112
+ ablo.weatherReports.retrieve('report_stockholm');
91
113
 
92
- // Synchronous, from the local cache → row[]
93
114
  const pending = ablo.weatherReports.list({
94
- where: { status: 'pending' }, // equality filter (an array value means IN)
115
+ where: { status: 'pending' },
95
116
  orderBy: { location: 'asc' },
96
117
  limit: 20,
97
118
  });
98
119
 
99
- // Server fetch → Promise<row[]>. 'complete' waits for the server; 'unknown'
100
- // returns what's local now and refreshes in the background.
101
120
  const ready = await ablo.weatherReports.load({
102
121
  where: { status: 'ready' },
103
122
  type: 'complete',
104
123
  });
105
124
  ```
106
125
 
126
+ An array value in `where` means `IN`. On `load`, `type: 'complete'` waits for
127
+ the server; `'unknown'` returns what's local now and refreshes in the background.
128
+
107
129
  ## Writing
108
130
 
109
131
  `create` / `update` apply optimistically and resolve to the row. Two options
@@ -128,30 +150,35 @@ meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
128
150
 
129
151
  ```ts
130
152
  await ablo.weatherReports.claim('report_stockholm', async (report) => {
131
- // If someone else holds it, claim() WAITS in a fair queue, then re-reads —
132
- // so `report` is the current row, never a stale snapshot. Reads stay open by
133
- // default; only acting-on-the-row serializes.
134
-
135
- const forecast = await weatherAgent.getWeather(report.location); // slow LLM gap
153
+ const forecast = await weatherAgent.getWeather(report.location);
136
154
  await ablo.weatherReports.update(report.id, { forecast, status: 'ready' });
137
- }); // claim released here, whether the callback returns or throws
155
+ });
138
156
  ```
139
157
 
158
+ If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
159
+ so `report` is the current row, never a stale snapshot. Reads stay open by
160
+ default; only acting on the row serializes. The claim releases when the callback
161
+ returns or throws.
162
+
140
163
  See who's mid-edit before you act — decide to wait, or skip:
141
164
 
142
165
  ```ts
143
- ablo.weatherReports.claimState('report_stockholm'); // → the holder, or null
144
- ablo.weatherReports.queue('report_stockholm'); // → { data: [{ heldBy, action, position }, …] }
166
+ ablo.weatherReports.claimState('report_stockholm');
167
+ ablo.weatherReports.queue('report_stockholm');
145
168
 
146
169
  await ablo.weatherReports.claim(id, async (report) => {
147
170
  /* do the held work */
148
- }, { wait: false }); // held? skip (dedup) — throws instead of waiting
171
+ }, { wait: false });
149
172
 
150
173
  await ablo.weatherReports.claim(id, async (report) => {
151
174
  /* do the held work */
152
- }, { maxQueueDepth: 2 }); // 2+ already ahead? bail
175
+ }, { maxQueueDepth: 2 });
153
176
  ```
154
177
 
178
+ `claimState` returns the holder (or `null`); `queue` returns the line waiting
179
+ behind it. `wait: false` skips rather than waiting when the row is held;
180
+ `maxQueueDepth: 2` bails when two or more are already ahead.
181
+
155
182
  Default reads keep working while a row is claimed. Server reads that need claimed
156
183
  semantics can opt in with `ifClaimed: 'return' | 'wait' | 'fail'`.
157
184
 
@@ -179,7 +206,7 @@ everything inside is live.
179
206
 
180
207
  ```tsx
181
208
  import { AbloProvider, useAblo } from '@abloatai/ablo/react';
182
- import { schema } from './ablo.schema';
209
+ import { schema } from './ablo/schema';
183
210
 
184
211
  function App() {
185
212
  return (
@@ -190,14 +217,11 @@ function App() {
190
217
  }
191
218
 
192
219
  function Report({ id }: { id: string }) {
193
- // Reactive read: this re-renders whenever the row changes — whether you,
194
- // a teammate, or an agent changed it.
195
220
  const report = useAblo((ablo) => ablo.weatherReports.retrieve(id));
196
221
  const ablo = useAblo();
197
222
 
198
223
  if (!report) return null;
199
224
 
200
- // Write: same method as the server example above. Optimistic; fans out.
201
225
  return (
202
226
  <button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
203
227
  {report.status}
@@ -206,6 +230,10 @@ function Report({ id }: { id: string }) {
206
230
  }
207
231
  ```
208
232
 
233
+ The `useAblo(selector)` read re-renders whenever the row changes — whether you,
234
+ a teammate, or an agent changed it. The write is the same optimistic, fan-out
235
+ method as the server example above.
236
+
209
237
  `<AbloProvider>` owns the connection — no API key in the browser. That's the
210
238
  whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
211
239
  other client (human or agent) on that row sees it in real time. See
@@ -226,8 +254,9 @@ the browser connects as an already-scoped participant — it never holds the key
226
254
  and can't widen its own scope. Your schema's `identityRoles` map that identity
227
255
  to sync-group strings.
228
256
 
257
+ `userId` / `teamIds` come from your auth, resolved server-side:
258
+
229
259
  ```tsx
230
- // userId / teamIds come from YOUR auth, resolved server-side
231
260
  <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
232
261
  <App />
233
262
  </AbloProvider>
@@ -262,7 +291,7 @@ HTTP endpoint when a server-to-server caller needs to write without opening a
262
291
  WebSocket:
263
292
 
264
293
  ```bash
265
- curl https://api.ablo.dev/v1/commits \
294
+ curl https://api.abloatai.com/v1/commits \
266
295
  -H "Authorization: Bearer sk_test_..." \
267
296
  -H "Content-Type: application/json" \
268
297
  -d '{ "operations": [
@@ -662,7 +662,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
662
662
  */
663
663
  queryByClass(modelClass: ModelConstructor<Model>, options?: {
664
664
  predicate?: (model: Model) => boolean;
665
- scope?: ModelScope;
665
+ state?: ModelScope;
666
666
  orderBy?: keyof Model;
667
667
  order?: 'asc' | 'desc';
668
668
  limit?: number;
@@ -1688,7 +1688,7 @@ export class BaseSyncedStore {
1688
1688
  const modelName = this.objectPool.registry.getModelNameFromConstructor(modelClass);
1689
1689
  if (!modelName)
1690
1690
  return { data: [], total: 0, hasMore: false };
1691
- let allModels = this.objectPool.getByType(modelClass, options?.scope ?? ModelScope.live);
1691
+ let allModels = this.objectPool.getByType(modelClass, options?.state ?? ModelScope.live);
1692
1692
  // Filter out pending deletes
1693
1693
  allModels = allModels.filter((m) => !this.pendingDeletes.has(m.id));
1694
1694
  // Apply predicate
@@ -784,6 +784,7 @@ export declare namespace Ablo {
784
784
  type ActiveIntent = _Streams.ActiveIntent;
785
785
  type Claim = _Streams.Claim;
786
786
  type IntentRejection = _Streams.IntentRejection;
787
+ type IntentLost = _Streams.IntentLost;
787
788
  type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
788
789
  type Turn = import('./Ablo.js').Turn;
789
790
  namespace Auth {
@@ -1211,6 +1211,7 @@ export function Ablo(options) {
1211
1211
  entities: { [modelKey]: id },
1212
1212
  }),
1213
1213
  queue: (target) => publicIntents.queueFor({ type: target.model, id: target.id }),
1214
+ reorder: (target, order) => publicIntents.reorder({ type: target.model, id: target.id }, order),
1214
1215
  observe: (target) => {
1215
1216
  // The live intent stream only tracks *open* (active) claims;
1216
1217
  // terminal states (committed / expired / canceled) drop out of
@@ -35,10 +35,12 @@ export interface ModelListOptions<T> {
35
35
  };
36
36
  limit?: number;
37
37
  offset?: number;
38
- /** Lifecycle scope. Defaults to live rows. */
39
- scope?: ModelListScope;
38
+ /** Lifecycle filter `live` (default), `archived`, or `all`. Named `state`
39
+ * (GitHub's open/closed/all precedent) so it doesn't collide with the
40
+ * sync-group `scope`. */
41
+ state?: ModelListScope;
40
42
  }
41
- export type ModelCountOptions<T> = Pick<ModelListOptions<T>, 'where' | 'filter' | 'scope'>;
43
+ export type ModelCountOptions<T> = Pick<ModelListOptions<T>, 'where' | 'filter' | 'state'>;
42
44
  export interface ModelLoadOptions<T> {
43
45
  /**
44
46
  * Filter for the lookup. Accepts:
@@ -109,6 +111,14 @@ export interface ModelCollaboration<T> {
109
111
  model: string;
110
112
  id: string;
111
113
  }): readonly Intent[];
114
+ /**
115
+ * Re-rank the wait queue on a target (privileged — server-gated). `order` is
116
+ * the desired front-of-line ordering, taken from `queue(target)`.
117
+ */
118
+ reorder(target: {
119
+ model: string;
120
+ id: string;
121
+ }, order: readonly Intent[]): void;
112
122
  /**
113
123
  * Resolve once no participant holds an active intent on the target.
114
124
  * The contender's "wait until it's free" — delegates to the intent
@@ -226,6 +236,19 @@ export interface ModelOperations<T, CreateInput> {
226
236
  readonly object: 'list';
227
237
  readonly data: readonly Intent[];
228
238
  };
239
+ /**
240
+ * Re-rank the wait queue on a record — move waiters to the front in the
241
+ * given order. Pass the `Intent[]` from `queue(id).data`, reordered. A
242
+ * privileged operation: the server gates it (the caller needs the
243
+ * `intent.reorder` capability), so it's fire-and-forget — the new order
244
+ * arrives reactively through `queue(id)`.
245
+ *
246
+ * ```ts
247
+ * const { data } = ablo.decks.queue('deck_1');
248
+ * ablo.decks.reorder('deck_1', [data[2], data[0], data[1]]); // promote #2
249
+ * ```
250
+ */
251
+ reorder(id: string, order: readonly Intent[]): void;
229
252
  /** Release a claim you hold early. Usually implicit (scope exit). */
230
253
  release(id: string): Promise<void>;
231
254
  /** Listen for changes (callback called on every change). */
@@ -134,7 +134,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
134
134
  return objectPool.get(id);
135
135
  },
136
136
  list(options) {
137
- const all = objectPool.getByType(ModelClass, (options?.scope ?? ModelScope.live));
137
+ const all = objectPool.getByType(ModelClass, (options?.state ?? ModelScope.live));
138
138
  let result = all;
139
139
  if (options?.where) {
140
140
  const where = options.where;
@@ -220,6 +220,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
220
220
  data: collaboration?.queue({ model: schemaKey, id }) ?? [],
221
221
  };
222
222
  },
223
+ reorder(id, order) {
224
+ collaboration?.reorder({ model: schemaKey, id }, order);
225
+ },
223
226
  release(id) {
224
227
  return releaseClaim(id);
225
228
  },
@@ -15,7 +15,7 @@ export function validateAbloOptions(input) {
15
15
  const kind = options.kind ?? 'user';
16
16
  if (!url) {
17
17
  return new Error('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
18
- `Ablo({ baseURL: 'wss://sync.ablo.dev', schema, user })`);
18
+ `Ablo({ baseURL: 'wss://sync.abloatai.com', schema, user })`);
19
19
  }
20
20
  // Schema is optional for the model-first API:
21
21
  // Ablo({ apiKey }).model('clauses').retrieve(...)
@@ -37,7 +37,7 @@ export function validateAbloOptions(input) {
37
37
  if (!configuredApiKey && !configuredAuthToken && kind === 'agent' && !options.capabilityToken) {
38
38
  return new Error('Ablo: provide either `apiKey` (hosted cloud — SDK exchanges internally) ' +
39
39
  'or `capabilityToken` (self-hosted — your auth layer mints + hands in). ' +
40
- 'See https://ablo.dev/docs/api-keys for the full pattern.');
40
+ 'See https://abloatai.com/docs/api-keys for the full pattern.');
41
41
  }
42
42
  return null;
43
43
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@abloatai/ablo/coordination` — the canonical wire schema for the three
3
+ * coordination layers (presence, pessimistic claims, optimistic stale-context).
4
+ * See `./schema.ts` for the model and the per-layer schemas.
5
+ */
6
+ export * from './schema.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@abloatai/ablo/coordination` — the canonical wire schema for the three
3
+ * coordination layers (presence, pessimistic claims, optimistic stale-context).
4
+ * See `./schema.ts` for the model and the per-layer schemas.
5
+ */
6
+ export * from './schema.js';