@abloatai/ablo 0.57.0 → 0.59.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 (90) hide show
  1. package/AGENTS.md +12 -5
  2. package/CHANGELOG.md +269 -13
  3. package/README.md +6 -5
  4. package/dist/ai-sdk.d.ts +1 -1
  5. package/dist/ai-sdk.d.ts.map +1 -1
  6. package/dist/context/evidence.d.ts +6 -8
  7. package/dist/context/evidence.d.ts.map +1 -1
  8. package/dist/context/evidence.js +6 -20
  9. package/dist/context/evidence.js.map +1 -1
  10. package/dist/context/index.d.ts +23 -0
  11. package/dist/context/index.d.ts.map +1 -0
  12. package/dist/context/index.js +26 -0
  13. package/dist/context/index.js.map +1 -0
  14. package/dist/context/onChange.d.ts +9 -0
  15. package/dist/context/onChange.d.ts.map +1 -0
  16. package/dist/context/onChange.js +37 -0
  17. package/dist/context/onChange.js.map +1 -0
  18. package/dist/source-conformance.d.ts +1 -1
  19. package/dist/source-conformance.d.ts.map +1 -1
  20. package/dist/source-conformance.js +1 -1
  21. package/dist/source-conformance.js.map +1 -1
  22. package/dist/source-drizzle.d.ts +1 -1
  23. package/dist/source-drizzle.d.ts.map +1 -1
  24. package/dist/source-drizzle.js +1 -1
  25. package/dist/source-drizzle.js.map +1 -1
  26. package/dist/source-kysely.d.ts +1 -1
  27. package/dist/source-kysely.d.ts.map +1 -1
  28. package/dist/source-kysely.js +1 -1
  29. package/dist/source-kysely.js.map +1 -1
  30. package/dist/source-next.d.ts +1 -1
  31. package/dist/source-next.d.ts.map +1 -1
  32. package/dist/source-next.js +1 -1
  33. package/dist/source-next.js.map +1 -1
  34. package/docs/agents.md +38 -15
  35. package/docs/api-keys.md +6 -6
  36. package/docs/api.md +107 -34
  37. package/docs/basic-usage.md +84 -0
  38. package/docs/branch-development.md +23 -4
  39. package/docs/cli.md +16 -9
  40. package/docs/client-behavior.md +27 -29
  41. package/docs/comparison.md +63 -0
  42. package/docs/concurrency-convention.md +92 -75
  43. package/docs/context.md +76 -31
  44. package/docs/coordinate-existing-work.md +104 -0
  45. package/docs/coordination.md +93 -38
  46. package/docs/data-sources.md +12 -6
  47. package/docs/debugging.md +1 -1
  48. package/docs/deployment.md +19 -1
  49. package/docs/examples/agent-human.md +6 -18
  50. package/docs/examples/coordination-conformance.md +69 -0
  51. package/docs/examples/evidence-backed-document-pipeline.md +488 -0
  52. package/docs/examples/existing-python-backend.md +10 -13
  53. package/docs/examples/nextjs.md +2 -2
  54. package/docs/examples/scoped-agent.md +18 -1
  55. package/docs/examples/server-agent.md +2 -2
  56. package/docs/faq.md +75 -0
  57. package/docs/groups.md +19 -139
  58. package/docs/guarantees.md +8 -8
  59. package/docs/idempotency.md +3 -0
  60. package/docs/identity.md +2 -1
  61. package/docs/implement.md +61 -0
  62. package/docs/implementation-index.md +20 -0
  63. package/docs/index.md +59 -173
  64. package/docs/installation.md +77 -0
  65. package/docs/instrumentation.md +52 -0
  66. package/docs/integration-guide.md +20 -19
  67. package/docs/integrations/sandbox-runtime.md +157 -0
  68. package/docs/integrations.md +9 -0
  69. package/docs/migration.md +12 -7
  70. package/docs/operating-on-your-database.md +7 -0
  71. package/docs/options.md +172 -0
  72. package/docs/quickstart.md +25 -14
  73. package/docs/react.md +9 -9
  74. package/docs/schema-contract.md +14 -13
  75. package/docs/security.md +64 -0
  76. package/docs/sessions.md +1 -1
  77. package/examples/README.md +8 -2
  78. package/examples/agent-turn.ts +1 -1
  79. package/examples/expensive-agent-turn.ts +1 -1
  80. package/examples/stale-context-agent-turn.ts +106 -0
  81. package/llms.txt +23 -12
  82. package/package.json +7 -6
  83. package/dist/context/sources.d.ts +0 -21
  84. package/dist/context/sources.d.ts.map +0 -1
  85. package/dist/context/sources.js +0 -36
  86. package/dist/context/sources.js.map +0 -1
  87. package/dist/context.d.ts +0 -22
  88. package/dist/context.d.ts.map +0 -1
  89. package/dist/context.js +0 -33
  90. package/dist/context.js.map +0 -1
package/docs/faq.md ADDED
@@ -0,0 +1,75 @@
1
+ # FAQ
2
+
3
+ > Short answers to the implementation choices developers encounter most often.
4
+
5
+ ## Does Ablo replace PostgreSQL?
6
+
7
+ No. PostgreSQL remains the durable source of truth. Keep its schema, constraints,
8
+ transactions, row-level security, and short write locks. Ablo coordinates work
9
+ around that state and can route guarded writes into it.
10
+
11
+ ## Does Ablo replace Redis locks?
12
+
13
+ It can replace an application-owned reservation layer, but it does not require a
14
+ rewrite. Ablo standardizes resource identity, participant identity, leases,
15
+ waiting, recovery, stale-work rejection, and visibility. Teams can begin by
16
+ coordinating an existing operation while its final transaction remains in the
17
+ application.
18
+
19
+ ## Is Ablo only for agents?
20
+
21
+ No. Agents, workers, application services, and people can coordinate over the
22
+ same resources. The package-root client is suited to stateless server work; the
23
+ [React client](./react.md) adds live state and presence for human interfaces.
24
+
25
+ ## Which client should I import?
26
+
27
+ Use the default export from `@abloatai/ablo` for agents, workers, route handlers,
28
+ and server operations. It uses ordinary request/response HTTP. Use
29
+ `@abloatai/ablo/react` when a live interface needs local synchronized state,
30
+ subscriptions, or presence.
31
+
32
+ ## What is the difference between get and read?
33
+
34
+ `get({ id })` observes the current row. `read({ id })` captures the exact row
35
+ version as evidence for a later guarded write. Pass that returned row in the
36
+ mutation's `reads` array when the decision must be rejected if its premise
37
+ changed.
38
+
39
+ ## When should I claim a resource?
40
+
41
+ Claim before slow or expensive work when another participant should not perform
42
+ conflicting work on the same business resource. Do not add a claim to every
43
+ write: a short, independent update can use its normal database and mutation
44
+ semantics.
45
+
46
+ ## Is a claim a database lock?
47
+
48
+ No. A claim is a participant-scoped lease held across work that may outlive one
49
+ database transaction. It expires after heartbeat loss. PostgreSQL locks still
50
+ protect the short authoritative transaction.
51
+
52
+ ## What if code writes directly to PostgreSQL?
53
+
54
+ Ablo observes the resulting change through the configured data source, but the
55
+ writer bypasses Ablo claims and request ordering. Keep database constraints for
56
+ rules that must apply to every writer.
57
+
58
+ ## Are retries exactly once?
59
+
60
+ Ablo idempotency deduplicates the same Ablo request within its retention window.
61
+ It does not make external side effects exactly once. Use the external provider's
62
+ idempotency mechanism or an application-owned effect record.
63
+
64
+ ## Does Ablo run long workflows?
65
+
66
+ No. Temporal, Inngest, queues, and application workers still own scheduling,
67
+ retries, and durable workflow progress. Ablo coordinates the shared state those
68
+ executions read and change.
69
+
70
+ ## Do I need to understand fencing first?
71
+
72
+ No. Start from the public behavior: a claim expires, another participant can
73
+ take over, and an obsolete owner cannot use an old claim to commit through Ablo.
74
+ The implementation mechanism is documented for operators and advanced
75
+ integrations, not required for basic SDK use.
package/docs/groups.md CHANGED
@@ -17,30 +17,28 @@ block `C`. Between those steps someone else edits `A`. The agent is now building
17
17
  `C` on a premise that has moved — and nothing about writing `C` looks wrong in
18
18
  isolation. That is stale context, and it is the thing sync groups let you catch.
19
19
 
20
- The recipe is one field on the commit: declare the group you read as a premise,
21
- and say what should happen if it moved.
20
+ The recipe is one field on the commit: declare the group you read as a premise.
22
21
 
23
22
  ```ts
24
23
  // The agent read everything under workspace:abc to compose this write.
25
24
  await ablo.blocks.update({
26
25
  id: 'block-C',
27
26
  data: { text: revised },
28
- reads: [{ group: 'workspace:abc', readAt: watermark, onStale: 'notify' }],
27
+ reads: [{ group: 'workspace:abc', readAt: watermark }],
29
28
  });
30
29
  ```
31
30
 
32
31
  At commit, inside the write transaction, the engine asks a single question: *did
33
32
  any delta routed to `workspace:abc` land after `watermark`?* If nothing moved, the
34
- write applies. If something moved, `onStale` decides `notify` holds the write
35
- and hands the agent a `StaleNotification` naming the group, so it re-reads
36
- `workspace:abc` and regenerates; `reject` aborts the batch with a `409`. The agent
37
- never persists work built on a premise it can no longer see.
33
+ write applies. If something moved, Ablo rejects the batch with a `409`, so the
34
+ agent can re-read `workspace:abc`, regenerate, and submit a fresh guarded
35
+ write. The agent never persists work built on a premise it can no longer see.
38
36
 
39
37
  ---
40
38
 
41
39
  ## How you hear about it
42
40
 
43
- Four channels carry "something changed", and they answer four different
41
+ Three channels carry "something changed", and they answer three different
44
42
  questions. Pick by the question you have.
45
43
 
46
44
  ```ts
@@ -52,10 +50,9 @@ await using room = await ablo.records.join(documentIds, { ttl: '5m' });
52
50
  room.peers;
53
51
 
54
52
  // Stop this write if the thing I read moved while I composed it.
55
- await ablo.blocks.update({ id, data, reads: [{ group: 'workspace:abc', readAt, onStale: 'notify' }] });
56
-
57
- // Tell me later if this moves, even though I am not writing now.
58
- await ablo.records.track({ id: 's-1' });
53
+ const record = await ablo.records.read({ id: 's-1' });
54
+ if (!record) throw new Error('Record not found');
55
+ await ablo.blocks.update({ id, data, reads: [record] });
59
56
  ```
60
57
 
61
58
  | Question | Channel | Arrives |
@@ -63,29 +60,12 @@ await ablo.records.track({ id: 's-1' });
63
60
  | What do the rows say right now? | `onChange` | As deltas land, on the socket |
64
61
  | Who else is working here? | `join`, then `room.peers` and `room.claims` | As participants come and go, on the socket |
65
62
  | Did the premise for **this** write move? | `reads` on the write | On that write's receipt, before it applies |
66
- | Has anything I read moved since? | `track` | On your next commit's receipt |
67
- | Stop me writing if it has | `track` with `onStale: 'reject'` | Refuses that commit, at the chokepoint |
68
-
69
- Two distinctions do most of the work here.
70
-
71
- **`join` is about people; `track` is about data.** Both open a subscription and
72
- both are scoped by sync group, which is why they look alike. `join` reports
73
- participants: who is present, what they are doing, which rows they hold. `track`
74
- reports the rows themselves: something you said you cared about moved, here is
75
- the watermark to re-read it at. A tool that wants to avoid duplicating a peer's
76
- work needs `join`. A tool whose output goes stale when its inputs change needs
77
- `track`.
78
-
79
- **`reads` guards one write; `track` outlives it.** They speak the same
80
- vocabulary and produce the same `StaleNotification`. A `reads` entry is checked
81
- once, at the commit that carried it, and discarded. A `track` is persisted and
82
- re-checked against every delta after it, so a long-running actor hears about a
83
- change that landed while it was thinking, on the next commit it makes.
84
63
 
85
64
  `onChange` and `join` need a live socket, so they are available on the default
86
- WebSocket client. `reads` and `track` ride the commit, so they reach a socketless
87
- actor over HTTP too, which is what makes them the notification path for agents
88
- and workers.
65
+ WebSocket client. `reads` rides the commit, so it reaches a socketless actor over
66
+ HTTP too. The row returned by `read` privately carries its model, id, and
67
+ watermark; passing that row in `reads` is enough to protect a later write. Ablo
68
+ does not retain the row contents as read evidence.
89
69
 
90
70
  ---
91
71
 
@@ -158,13 +138,12 @@ Two consequences worth designing around:
158
138
  ## Declaring the batch premise
159
139
 
160
140
  `reads[]` declares what the commit was based on. Each entry is a premise, and
161
- each governs the *whole* commit: if one goes stale, its disposition applies to
162
- every write in the batch, not just one operation. You choose the granularity per
163
- entry.
141
+ each governs the *whole* commit: if one goes stale, every write in the batch
142
+ rejects. You choose the granularity per entry.
164
143
 
165
144
  ```ts
166
145
  reads: [
167
- { group: 'workspace:abc', readAt: N, onStale: 'notify' }, // did anything in the workspace move?
146
+ { group: 'workspace:abc', readAt: N }, // did anything in the workspace move?
168
147
  { model: 'Document', id: 's-1', readAt: N, fields: ['title'] }, // did this row (this field) move?
169
148
  ]
170
149
  ```
@@ -175,108 +154,9 @@ literal: this object, optionally these fields. A row premise with `fields`
175
154
  conflicts only on real field overlap, so two actors editing disjoint fields of
176
155
  the same row don't collide.
177
156
 
178
- `onStale` has three settings, defaulting to `reject`:
179
-
180
- - **`notify`** holds every write in the batch and returns a `StaleNotification`.
181
- For a group premise it carries the group name and the new watermark
182
- (`observedSyncId`); re-read the group at that point and regenerate. This is the
183
- setting a chain wants — the actor gets the truth and resolves it.
184
- - **`reject`** aborts the batch with a `stale_context` error (`409`). The right
185
- default when there is nothing to reconcile and the write should simply not
186
- land.
187
- - **`overwrite`** skips the check and lets the write land — last-write-wins, the
188
- explicit escape hatch.
189
-
190
- ---
191
-
192
- ## Staying subscribed across commits: `track`
193
-
194
- A batch premise guards a single commit: you state what you read, the engine
195
- checks it, the premise is gone. That fits an actor that reads and writes in one
196
- breath. It does not fit a long-running one — an agent that reads a row now,
197
- works for a few minutes, and writes much later. By the time it commits, the
198
- premise it would have declared is stale, and there was no commit in between on
199
- which to hear that the ground had shifted.
200
-
201
- `track` is the durable half of the same idea. Register what you are watching and
202
- it persists on the server; the next time you commit anything, a change that
203
- landed on the tracked target since you registered rides back on your receipt —
204
- the same `StaleNotification` an `onStale: 'notify'` premise would have handed
205
- you, arriving on the write you were going to make anyway.
206
-
207
- ```ts
208
- // Register interest and walk away — no write required.
209
- await ablo.records.track({ id: 's-1' });
210
-
211
- // …minutes of other work later, on your next commit…
212
- const res = await ablo.blocks.update({ id: 'block-C', data: { text: revised } });
213
- res.notifications; // populated if s-1 moved under you in the meantime
214
- ```
215
-
216
- The target is a row (`{ id }` on the model verb) or a sync group (as a write
217
- option, below). A track is an idempotent registration: calling it again refreshes
218
- the same subscription rather than stacking duplicates, and once a change fires the
219
- track re-baselines, so the same change notifies once. Your own writes to a target
220
- you track never notify you — the signal is about what *others* did.
221
-
222
- You can also register a track as part of a write you are already making, the
223
- persisted companion to `reads`:
224
-
225
- ```ts
226
- await ablo.records.update({
227
- id: 's-1',
228
- data: { title: revised },
229
- reads: [{ group: 'workspace:abc', readAt: N, onStale: 'notify' }], // guards THIS commit
230
- track: [{ group: 'workspace:abc' }], // and keeps watching after it
231
- });
232
- ```
233
-
234
- So `reads` is the premise for the commit in hand; `track` is a standing
235
- subscription that outlives it. Both speak the same notification vocabulary.
236
- Delivery is on your next commit's receipt; a track does not yet push out of band
237
- between commits.
238
-
239
- ### Reporting, or gating: `onStale`
240
-
241
- A track says what a moved belief should do to your **next write**. Same enum the
242
- `reads` premise carries, minus the one mode that cannot apply:
243
-
244
- ```ts
245
- await ablo.records.track({ id: 's-1' }); // notify (default)
246
- await ablo.records.track({ id: 's-1', onStale: 'reject' }); // gate
247
- ```
248
-
249
- - **`notify`** — the change rides your next receipt and the commit proceeds. You
250
- decide what it means.
251
- - **`reject`** — your next commit is **refused** while that belief is stale, even
252
- if it writes an unrelated row. The gate is on what you *know*, not on what you
253
- are touching.
254
-
255
- `reject` is for an actor that must not act on a stale picture — an agent that
256
- read a row, reasoned for minutes, and is about to write based on what it
257
- believed. Ablo enforces it at the commit chokepoint, so it is a guarantee rather
258
- than a convention: the write cannot land.
259
-
260
- The gate takes a key. It does **not** reopen on its own, because an agent that
261
- retried blindly would land exactly the write `reject` was asked to prevent. You
262
- re-read, then re-register the track to say so:
263
-
264
- ```ts
265
- try {
266
- await ablo.records.update({ id, data });
267
- } catch (err) {
268
- if (err.code === 'stale_context') {
269
- const fresh = await ablo.records.get({ id: 's-1' }); // read
270
- await ablo.records.track({ id: 's-1', onStale: 'reject' }); // acknowledge
271
- await ablo.records.update({ id, data: reconsider(fresh) }); // now it lands
272
- }
273
- }
274
- ```
275
-
276
- A track-only call is never gated, so acknowledging is always reachable.
277
-
278
- `overwrite` is not offered: it means "apply my write anyway", and a track guards
279
- no write of its own to apply.
157
+ Any stale premise aborts the batch with `stale_context` (`409`). Re-read and
158
+ regenerate if the work is still relevant. Omit `reads` only when the write is
159
+ intentionally unconditional.
280
160
 
281
161
  ---
282
162
 
@@ -44,11 +44,11 @@ The server remains the source of truth.
44
44
 
45
45
  ## Stale-Write Protection
46
46
 
47
- Use `snapshot(...)` and `readAt` when a write depends on state the agent already
48
- read:
47
+ Use `read(...)` and carry the returned row in `reads` when a write depends on
48
+ state the agent already read:
49
49
 
50
50
  ```ts
51
- const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
51
+ const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
52
52
  if (!report) throw new Error('report missing');
53
53
 
54
54
  await ablo.weatherReports.update({
@@ -67,7 +67,7 @@ not acquire this guarantee.
67
67
 
68
68
  Two other dispositions exist. `overwrite` applies the write with no stale check
69
69
  at all. `notify` **holds** the write, so the row is left as it stands, and hands
70
- back a `StaleNotification` carrying the current value for the actor to reconcile
70
+ back an `AbloStaleContextError` so the actor can re-read and reconcile
71
71
  and re-issue; the rest of the batch still commits.
72
72
 
73
73
  See [Concurrency Convention](./concurrency-convention.md) for the full taxonomy,
@@ -96,10 +96,10 @@ claim queues fairly behind the holder).
96
96
 
97
97
  By default, a held claim rejects writes from other participants to the claimed
98
98
  target. Contenders that call `claim` wait their turn; ordinary reads remain
99
- open. An explicit model conflict policy can choose another disposition for a
100
- participant kind. While you hold a claim, the matching
101
- `ablo.<model>.update({ id, ... })` is rejected with `AbloStaleContextError` if
102
- the row changed underneath you after your claim point.
99
+ open. While you hold a claim, the matching
100
+ `ablo.<model>.update({ id, ..., claim })` proves ownership at write time and is
101
+ rejected with `AbloStaleContextError` if the row changed underneath you after
102
+ your claim point. Do not omit `claim` from a row-backed claimed write.
103
103
 
104
104
  ## Agent Runs
105
105
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  > Make a retried write safe: the same key never applies the same change twice.
4
4
 
5
+ This page owns idempotency for updates and retries, including changed intent
6
+ and the `idempotency_conflict` failure code.
7
+
5
8
  An agent retries. A socket drops mid-commit, a worker restarts, a queue redelivers — and the write
6
9
  you already sent arrives again. An idempotency key is how Ablo tells a retry from a new intention.
7
10
 
package/docs/identity.md CHANGED
@@ -410,7 +410,7 @@ it to the provider:
410
410
 
411
411
  ```ts
412
412
  // lib/ablo.ts
413
- import Ablo from '@abloatai/ablo';
413
+ import { Ablo } from '@abloatai/ablo/react';
414
414
  import { schema } from '@/ablo/schema';
415
415
 
416
416
  // Build the client from the identity your server already resolved.
@@ -505,6 +505,7 @@ subset of what its user could see:
505
505
  // (the floor). Build each group from the model's scope with `syncGroup(kind, id)`.
506
506
  const session = await server.sessions.create({
507
507
  agent: { id: agentId },
508
+ onBehalfOf: { user: { id: triggeringUser.id } },
508
509
  can: { Document: ['read', 'update'], Workspace: ['read', 'update'] },
509
510
  syncGroups: [syncGroup('document', recordId), syncGroup('workspace', workspaceId)],
510
511
  });
@@ -0,0 +1,61 @@
1
+ # Choose the Ablo operation
2
+
3
+ > Find one implementation route from the work you are doing, copy its smallest recipe, and open guarantee details only when you need recovery behavior.
4
+
5
+ Do not read the documentation front to back. Start with the operation your
6
+ application already has, then use one row below.
7
+
8
+ | What you are implementing | Start here | Add only when |
9
+ |---|---|---|
10
+ | Get one row by id or list rows with the installed client | [Basic usage](./basic-usage.md) | Use `read`, not `get`, only when a later Ablo write depends on that exact row. |
11
+ | Configure a stateless worker's identity, permissions, or lifecycle | [Agents](./agents.md) | Keep the worker on HTTP; live human interfaces use React. |
12
+ | Wrap an existing API, service, Postgres transaction, filesystem write, or Git merge | [Coordinate existing work](./coordinate-existing-work.md) | Keep the final write in its existing owner. |
13
+ | Expose an existing named operation through GraphQL.js | [GraphQL.js](./approaches/graphql/graphql-js.md) | Keep the resolver dependent on that operation, not directly on Ablo. |
14
+ | Hold an Ablo model row while slow work runs, then write it through Ablo | [Coordination](./coordination.md) | Pass the returned claim handle to the write. |
15
+ | Reject a write when an earlier decision input changed | [Concurrency convention](./concurrency-convention.md) | Pass the exact object returned by `read` through `reads`. |
16
+ | Apply several Ablo writes all-or-none | [Atomic commits](./api.md#atomic-commits) | Put every operation and every captured premise in one `commits.create`. |
17
+ | Make a retried Ablo write safe | [Idempotency](./idempotency.md) | Derive one key from the business event and reuse it only for the identical request. |
18
+ | Send email, charge money, call a provider, or write a file | Keep that effect in the application | Use the provider's key or an application outbox; an Ablo key covers only the Ablo mutation. |
19
+ | Add a live human interface | [React](./react.md) | Humans use the WebSocket/live plane; stateless workers stay on HTTP. |
20
+
21
+ ## The four choices agents most often confuse
22
+
23
+ ```ts
24
+ // Observe one current row. No later stale check.
25
+ const task = await ablo.tasks.get({ id });
26
+
27
+ // Declare a premise for one later Ablo write.
28
+ const premise = await ablo.tasks.read({ id });
29
+ if (!premise) throw new Error('task not found');
30
+ await ablo.tasks.update({ id, data, reads: [premise] });
31
+
32
+ // Hold an Ablo row across slow work. The final write goes through Ablo.
33
+ await using claim = await ablo.tasks.claim({ id });
34
+ await ablo.tasks.update({ id, data, claim });
35
+
36
+ // Coordinate row-free work whose final write stays in the application.
37
+ await using lease = await ablo.taskRuns.claim(id, {
38
+ contention: { mode: 'skip' },
39
+ });
40
+ if (lease) await existingTaskService.complete(id);
41
+ ```
42
+
43
+ Use only one of those shapes unless the operation genuinely has both a claimed
44
+ target and separate captured premises. Claims answer who may work; `reads`
45
+ answer whether evidence is still current; `commits.create` answers whether
46
+ several Ablo writes land together; the existing database transaction still
47
+ owns atomicity for application-owned writes.
48
+
49
+ ## Before writing code
50
+
51
+ Answer these five questions:
52
+
53
+ 1. Which existing operation am I preserving?
54
+ 2. Does the final write belong to Ablo or to the application?
55
+ 3. Is the coordination identity a model row or only a stable business id?
56
+ 4. Which exact rows influenced the decision?
57
+ 5. Which writes must land together?
58
+
59
+ If an answer is unknown, preserve the existing operation and database boundary.
60
+ Do not introduce claims, captured reads, or atomic commits merely because they
61
+ exist.
@@ -0,0 +1,20 @@
1
+ # Ablo implementation index
2
+
3
+ > Route an implementation task to the smallest relevant Ablo guide before reading details.
4
+
5
+ Start with [Introduction](./index.md), then [Choose the Ablo operation](./implement.md). Its one routing table
6
+ separates ordinary reads, existing application writes, claims, captured reads,
7
+ atomic commits, retries, GraphQL, and external effects. Do not scan every page.
8
+
9
+ | Your task | Read first |
10
+ |---|---|
11
+ | Choose among nearby SDK and ownership patterns | [Choose the Ablo operation](./implement.md) |
12
+ | Add Ablo to existing work without replacing its API, transaction, filesystem write, or Git merge | [Coordinate existing work](./coordinate-existing-work.md) |
13
+ | Install Ablo and create a typed client | [Installation](./installation.md) |
14
+ | Read, write, and coordinate shared state | [Basic usage](./basic-usage.md) |
15
+ | Choose between a plain read, guarded read, claim, or atomic commit | [Concurrency convention](./concurrency-convention.md) |
16
+ | Look up an exact method, option, or error type | [API reference](./api.md) |
17
+ | Connect Ablo to an existing Postgres database | [Integration guide](./integration-guide.md) |
18
+
19
+ Follow links from that page only when its routing rule applies. Examples prove a
20
+ specific integration; they are not required reading for a first implementation.