@toclocoinc/lattice-grid 1.22.0 → 1.23.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  dependencies, no build step required. Optional adapters for React, Vue, Svelte
5
5
  and Web Components ship alongside it.
6
6
 
7
- Version 1.22.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.23.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
package/docs/API.html CHANGED
@@ -2764,11 +2764,16 @@ app.get('/api/orders', async (req, res) => {
2764
2764
  A worked example, executed on every build so it cannot go stale
2765
2765
  (<code>data-run</code>, PRD&nbsp;§7&nbsp;C3):
2766
2766
  </p>
2767
- <pre data-run="js" data-expect="3" data-covers="export:capabilitiesOf export:splitFilters"><code><span class="kw">const</span> { capabilitiesOf, splitFilters } = <span class="kw">await</span> import('../packages/core/src/source/pushdown.js');
2767
+ <pre data-run="js" data-expect="3" data-covers="export:capabilitiesOf export:splitFilters export:resolveMutate"><code><span class="kw">const</span> { capabilitiesOf, splitFilters, resolveMutate } = <span class="kw">await</span> import('../packages/core/src/source/pushdown.js');
2768
2768
 
2769
2769
  <span class="cmt">// An adapter that understands three comparisons and nothing else.</span>
2770
2770
  <span class="kw">const</span> caps = capabilitiesOf({ filter: 'tree', operators: ['eq', 'gt', 'lt'] });
2771
2771
 
2772
+ <span class="cmt">// Read-only by declaration: an adapter that says nothing about writing</span>
2773
+ <span class="cmt">// cannot mutate, and one that opts in resolves to a complete capability.</span>
2774
+ <span class="kw">if</span> (resolveMutate() !== false) <span class="kw">throw</span> new Error('a silent adapter must stay read-only');
2775
+ <span class="kw">if</span> (!resolveMutate({ update: true }).update) <span class="kw">throw</span> new Error('an opt-in must resolve');
2776
+
2772
2777
  <span class="cmt">// A conjunction splits: what the engine knows goes to it, the rest stays here.</span>
2773
2778
  <span class="kw">const</span> { pushed } = splitFilters({
2774
2779
  op: 'and',
@@ -2792,6 +2797,7 @@ app.get('/api/orders', async (req, res) =&gt; {
2792
2797
  <thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
2793
2798
  <tbody>
2794
2799
  <tr><td class="name">capabilitiesOf</td><td class="type">(declared?) =&gt; Required&lt;PushdownCapabilities&gt;</td><td class="desc">Resolves what an adapter declared against the defaults, giving a complete set with no absent keys to test for.</td></tr>
2800
+ <tr><td class="name">resolveMutate</td><td class="type">(declared?) =&gt; false | MutateCapability</td><td class="desc">Resolves an adapter's <code>mutate</code> declaration against the defaults. Returns <code>false</code> when the adapter cannot mutate, so a source over it stays read-only by declaration and refuses a write loudly rather than dropping it.</td></tr>
2795
2801
  <tr><td class="name">splitFilters</td><td class="type">(filters, caps) =&gt; { pushed, residual }</td><td class="desc">Divides a condition tree into the half the engine takes and the half left over. A conjunction splits; a disjunction that is not fully supported stays whole on the client, because pushing part of an <code>or</code> returns fewer rows than the filter allows and the grid cannot recover what was never fetched.</td></tr>
2796
2802
  <tr><td class="name">planQuery</td><td class="type">(request, caps) =&gt; PushdownPlan</td><td class="desc">Plans one request: the query to send, the work to finish afterwards, whether the whole result is needed, and which parts stayed behind.</td></tr>
2797
2803
  <tr><td class="name">applyResidual</td><td class="type">(rows, residual, compute) =&gt; unknown[]</td><td class="desc">Applies whatever the engine could not, through the grid's own filter and sort kernels rather than a second implementation, so a residual predicate means exactly what the same predicate means anywhere else.</td></tr>
@@ -3183,6 +3189,7 @@ off(); <span class="cmt">// on() returns i
3183
3189
  <tr><td class="name">cell:pending</td><td class="type">{ row, key, colId, value, before, id }</td><td class="desc">Applied optimistically, not yet durable. Only with <code>edit.commit</code>.</td></tr>
3184
3190
  <tr><td class="name">cell:confirmed</td><td class="type">{ row, key, colId, value, id, superseded }</td><td class="desc">The write reached the server.</td></tr>
3185
3191
  <tr><td class="name">cell:reverted</td><td class="type">{ row, key, colId, rejected, restored, reason, id, superseded, applied }</td><td class="desc">The write failed. <code>applied: false</code> means a newer edit owned the cell, so nothing was written back.</td></tr>
3192
+ <tr><td class="name">cell:conflict</td><td class="type">{ row, key, colId, value, serverRow, id }</td><td class="desc">The write succeeded but the server row had moved underneath it. Last-write-wins: <code>value</code> stands and <code>serverRow</code> carries the server's truth so the divergence is surfaced, never swallowed.</td></tr>
3186
3193
  <tr><td class="name">cell:edit:start</td><td class="type">{ row, key, colId, column }</td><td class="desc">An edit session opened.</td></tr>
3187
3194
  <tr><td class="name">cell:edit:end</td><td class="type">{ row, key, colId, valid, errors }</td><td class="desc">It closed: committed or cancelled.</td></tr>
3188
3195
  <tr><td class="name">cell:clicked</td><td class="type">{ row, key, index, colId, column, value, text, event }</td><td class="desc">A cell was clicked. Announcement only: nothing is consumed, so editing and selection behave unchanged.</td></tr>
@@ -4322,16 +4329,16 @@ grid.destroy();
4322
4329
  <p class="section-note">Each documented event is subscribed to and unsubscribed on every build. A consumer
4323
4330
  wiring a handler to a renamed event gets silence, which is indistinguishable from an event that
4324
4331
  has not fired yet — so the name is checked rather than left to be discovered.</p>
4325
- <pre data-run="js" data-expect="101" data-covers="event:cell:changed event:cell:clicked event:cell:confirmed event:cell:contextmenu event:cell:dblclicked event:cell:edit:end event:cell:edit:start event:cell:pending event:cell:reverted event:clipboard:copy event:column:filter:open event:column:grouped event:column:menu:open event:column:pivoted event:column:resized event:columns:changed event:columns:tagged event:comment:added event:comment:deleted event:comment:edited event:comment:failed event:comment:indexLoaded event:comment:resolved event:comment:threadClosed event:comment:threadOpened event:comment:unresolved event:destroy event:detail:toggled event:diff:changed event:diff:swapped event:export:progress event:facet:computed event:facet:expanded event:facet:failed event:facet:filtered event:form:closed event:form:error event:form:opened event:form:saved event:formatting:changed event:group:toggled event:header:contextmenu event:highlight:changed event:history:applied event:history:changed event:licence:changed event:page:changed event:permissions:changed event:presence:failed event:presence:joined event:presence:left event:presence:lockRefused event:presence:published event:presence:updated event:presentation:captured event:presentation:changed event:presentation:ended event:presentation:scale event:presentation:spotlight event:presentation:started event:presentation:view event:range:changed event:ready event:redaction:changed event:render:done event:render:first event:row:clicked event:row:copied event:row:dblclicked event:row:edit:end event:row:edit:start event:row:moved event:row:received event:row:sent event:rows:deferred event:rows:paused event:rows:queued event:rows:resumed event:scroll event:scroll:end event:selection:changed event:size:changed event:source:error event:stream:chunk event:stream:end event:stream:evicted event:timeline:attached event:timeline:detached event:timeline:seek event:timeline:seeking event:toolpanel:focus event:tree:loadAborted event:tree:loadFailed event:tree:loaded event:tree:loading event:view:applied event:view:default event:view:removed event:view:renamed event:view:saved event:views:changed"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4332
+ <pre data-run="js" data-expect="102" data-covers="event:cell:changed event:cell:clicked event:cell:confirmed event:cell:conflict event:cell:contextmenu event:cell:dblclicked event:cell:edit:end event:cell:edit:start event:cell:pending event:cell:reverted event:clipboard:copy event:column:filter:open event:column:grouped event:column:menu:open event:column:pivoted event:column:resized event:columns:changed event:columns:tagged event:comment:added event:comment:deleted event:comment:edited event:comment:failed event:comment:indexLoaded event:comment:resolved event:comment:threadClosed event:comment:threadOpened event:comment:unresolved event:destroy event:detail:toggled event:diff:changed event:diff:swapped event:export:progress event:facet:computed event:facet:expanded event:facet:failed event:facet:filtered event:form:closed event:form:error event:form:opened event:form:saved event:formatting:changed event:group:toggled event:header:contextmenu event:highlight:changed event:history:applied event:history:changed event:licence:changed event:page:changed event:permissions:changed event:presence:failed event:presence:joined event:presence:left event:presence:lockRefused event:presence:published event:presence:updated event:presentation:captured event:presentation:changed event:presentation:ended event:presentation:scale event:presentation:spotlight event:presentation:started event:presentation:view event:range:changed event:ready event:redaction:changed event:render:done event:render:first event:row:clicked event:row:copied event:row:dblclicked event:row:edit:end event:row:edit:start event:row:moved event:row:received event:row:sent event:rows:deferred event:rows:paused event:rows:queued event:rows:resumed event:scroll event:scroll:end event:selection:changed event:size:changed event:source:error event:stream:chunk event:stream:end event:stream:evicted event:timeline:attached event:timeline:detached event:timeline:seek event:timeline:seeking event:toolpanel:focus event:tree:loadAborted event:tree:loadFailed event:tree:loaded event:tree:loading event:view:applied event:view:default event:view:removed event:view:renamed event:view:saved event:views:changed"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4326
4333
 
4327
4334
  <span class="cmt">// Every documented event name, checked against the bus that would carry it.</span>
4328
4335
  <span class="cmt">// Subscribing to a name the grid does not know is the failure this catches:</span>
4329
4336
  <span class="cmt">// a consumer wiring a handler to a renamed event gets silence, and silence</span>
4330
4337
  <span class="cmt">// is indistinguishable from an event that simply has not fired yet.</span>
4331
4338
  <span class="kw">const</span> documented = [
4332
- 'cell:changed', 'cell:clicked', 'cell:confirmed', 'cell:contextmenu',
4333
- 'cell:dblclicked', 'cell:edit:end', 'cell:edit:start', 'cell:pending',
4334
- 'cell:reverted', 'clipboard:copy', 'column:filter:open', 'column:grouped',
4339
+ 'cell:changed', 'cell:clicked', 'cell:confirmed', 'cell:conflict',
4340
+ 'cell:contextmenu', 'cell:dblclicked', 'cell:edit:end', 'cell:edit:start',
4341
+ 'cell:pending', 'cell:reverted', 'clipboard:copy', 'column:filter:open', 'column:grouped',
4335
4342
  'column:menu:open', 'column:pivoted', 'column:resized', 'columns:changed',
4336
4343
  'columns:tagged', 'comment:added', 'comment:deleted', 'comment:edited',
4337
4344
  'comment:failed', 'comment:indexLoaded', 'comment:resolved', 'comment:threadClosed',
@@ -5545,7 +5552,7 @@ grid.destroy();
5545
5552
  <tr><td class="name">pasteInto</td><td class="type">(anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): number</td><td class="desc"></td></tr>
5546
5553
  <tr><td class="name">pastePreview</td><td class="type">boolean</td><td class="desc">Whether a bulk paste is previewed before it commits (`edit.pastePreview`, §12). <small>(read-only)</small></td></tr>
5547
5554
  <tr><td class="name">previewPaste</td><td class="type">(anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): {</td><td class="desc">Compute what a paste would change, without committing (§12). The engine behind `edit.pastePreview`: `changes` are the accepted writes with their old and new values (and whether each actually differs), `rejected` are the cells a commit would refuse, each with a reason.</td></tr>
5548
- <tr><td class="name">settle</td><td class="type">(id: string, ok: boolean, reason?: string): boolean</td><td class="desc"></td></tr>
5555
+ <tr><td class="name">settle</td><td class="type">(</td><td class="desc">Report the outcome of an in-flight write (§18.3; §5.1-5.2 reconcile). `reconcile` carries server truth on a successful settle: `value` is a server-authoritative value written back before `cell:confirmed` (`returning: 'row'`); `conflict.serverRow` surfaces a last-write-wins conflict via `cell:conflict`. Omit both to keep the optimistic value.</td></tr>
5549
5556
  <tr><td class="name">pending</td><td class="type">(): OpenWrite[]</td><td class="desc"></td></tr>
5550
5557
  <tr><td class="name">status</td><td class="type">(key: string, colId: string): 'pending' | null</td><td class="desc"></td></tr>
5551
5558
  </tbody>
@@ -6259,6 +6266,49 @@ grid.destroy();
6259
6266
  </tbody>
6260
6267
  </table>
6261
6268
  </div>
6269
+ <h3 id="type-MutateCapability">MutateCapability</h3>
6270
+ <p class="section-note">What an adapter can persist back to its source — the write-back capability (§4.1), declared on `AdapterCapabilities.mutate`. `false` (the default) is read-only by declaration; a resolved block turns every kind off unless the adapter opts in.</p>
6271
+ <div class="table-wrap">
6272
+ <table>
6273
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6274
+ <tbody>
6275
+ <tr><td class="name">append</td><td class="type">boolean</td><td class="desc">The adapter can insert new rows. Wave 1: declared, not yet bridged. <small>(optional)</small></td></tr>
6276
+ <tr><td class="name">update</td><td class="type">boolean</td><td class="desc">The adapter can patch existing rows. Wave 1: the wired kind (§4.3 Option A). <small>(optional)</small></td></tr>
6277
+ <tr><td class="name">delete</td><td class="type">boolean</td><td class="desc">The adapter can remove rows. Wave 1: declared, not yet bridged. <small>(optional)</small></td></tr>
6278
+ <tr><td class="name">returning</td><td class="type">'row' | 'key' | 'none'</td><td class="desc">The reconcile contract — what the server hands back after a successful mutation (§5.1). `'row'`: the authoritative row (id, computed columns, timestamps), reconciled before confirm. `'key'`: only the assigned key. `'none'` (the default): nothing — the optimistic value stands (last-write-wins). <small>(optional)</small></td></tr>
6279
+ </tbody>
6280
+ </table>
6281
+ </div>
6282
+ <h3 id="type-MutationOp">MutationOp</h3>
6283
+ <p class="section-note">One mutation handed to `adapter.mutate(op, request)` (§4.2). Cell-scoped `update` is the only kind wave 1 synthesises; `append`/`delete` are part of the shape so it survives into a later structural build (card 770).</p>
6284
+ <div class="table-wrap">
6285
+ <table>
6286
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6287
+ <tbody>
6288
+ <tr><td class="name">kind</td><td class="type">'append' | 'update' | 'delete'</td><td class="desc"></td></tr>
6289
+ <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="desc">append: the new rows (may lack a server-assigned key). <small>(optional)</small></td></tr>
6290
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">update: the row key. <small>(optional)</small></td></tr>
6291
+ <tr><td class="name">patch</td><td class="type">Record&lt;string, unknown&gt;</td><td class="desc">update: the changed columns only, matching `PendingWrite` semantics. <small>(optional)</small></td></tr>
6292
+ <tr><td class="name">keys</td><td class="type">string[]</td><td class="desc">delete: the row key(s). <small>(optional)</small></td></tr>
6293
+ <tr><td class="name">origin</td><td class="type">string</td><td class="desc">Provenance, carried through for auth / audit. <small>(optional)</small></td></tr>
6294
+ <tr><td class="name">requestId</td><td class="type">string</td><td class="desc">Stable id for idempotent retry / dedupe. Reserved; retry is a non-goal in wave 1. <small>(optional)</small></td></tr>
6295
+ </tbody>
6296
+ </table>
6297
+ </div>
6298
+ <h3 id="type-MutationResult">MutationResult</h3>
6299
+ <p class="section-note">The result of a mutation (§4.2) — the reconcile payload. A cell-update commit flows this back through `PendingWrites`: `ok: false` reverts and surfaces `reason`; `rows` (`returning: 'row'`) reconciles server truth before confirm; `conflict` surfaces a last-write-wins divergence via `cell:conflict`.</p>
6300
+ <div class="table-wrap">
6301
+ <table>
6302
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6303
+ <tbody>
6304
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
6305
+ <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="desc">`returning: 'row'` — the authoritative row(s) to reconcile to. <small>(optional)</small></td></tr>
6306
+ <tr><td class="name">keys</td><td class="type">string[]</td><td class="desc">`returning: 'key'` — server-assigned key(s) for appended rows, in order. <small>(optional)</small></td></tr>
6307
+ <tr><td class="name">reason</td><td class="type">string</td><td class="desc">On rejection — surfaced on `cell:reverted`, never swallowed. <small>(optional)</small></td></tr>
6308
+ <tr><td class="name">conflict</td><td class="type">{ key: string; serverRow?: unknown }</td><td class="desc">The server's current value, for a surfaced last-write-wins conflict. <small>(optional)</small></td></tr>
6309
+ </tbody>
6310
+ </table>
6311
+ </div>
6262
6312
  <h3 id="type-NumberFormat">NumberFormat</h3>
6263
6313
  <div class="table-wrap">
6264
6314
  <table>
@@ -6536,6 +6586,7 @@ grid.destroy();
6536
6586
  <tr><td class="name">name</td><td class="type">string</td><td class="desc">Used in diagnostics and in the message when work cannot be pushed. <small>(optional)</small></td></tr>
6537
6587
  <tr><td class="name">capabilities</td><td class="type">PushdownCapabilities</td><td class="desc"><small>(optional)</small></td></tr>
6538
6588
  <tr><td class="name">execute</td><td class="type">(query: RemoteRequest, request?: RemoteRequest):</td><td class="desc">Run the part of the query the adapter declared it could handle.</td></tr>
6589
+ <tr><td class="name">mutate</td><td class="type">(op: MutationOp, request?: RemoteRequest): Promise&lt;MutationResult&gt;</td><td class="desc">Persist one mutation (§4.2). Present only when `capabilities.mutate` opts in. `createPushdownSource` synthesises an `edit.commit` that calls this for cell updates (§4.3 Option A); `request` threads the abort signal through the way `execute` receives it, and auth already lives on the adapter. <small>(optional)</small></td></tr>
6539
6590
  </tbody>
6540
6591
  </table>
6541
6592
  </div>
@@ -6563,6 +6614,7 @@ grid.destroy();
6563
6614
  <tr><td class="name">range</td><td class="type">boolean</td><td class="desc">Whether the engine can return a window rather than the whole result. <small>(optional)</small></td></tr>
6564
6615
  <tr><td class="name">total</td><td class="type">boolean</td><td class="desc">Whether it can report the count of matching rows. <small>(optional)</small></td></tr>
6565
6616
  <tr><td class="name">group</td><td class="type">boolean</td><td class="desc">Whether it can group and aggregate. <small>(optional)</small></td></tr>
6617
+ <tr><td class="name">mutate</td><td class="type">false | MutateCapability</td><td class="desc">What the adapter can persist back — the write-back contract (§4.1). `false` (the default) is read-only by declaration. A declared block opts kinds in; `capabilitiesOf` resolves it to a full `MutateCapability` (or `false`). <small>(optional)</small></td></tr>
6566
6618
  </tbody>
6567
6619
  </table>
6568
6620
  </div>
@@ -437,7 +437,7 @@
437
437
  <div class="shell">
438
438
  <aside class="rail">
439
439
  <p class="rail__brand">Lattice Grid</p>
440
- <p class="rail__sub">Developer guide · v1.22.0</p>
440
+ <p class="rail__sub">Developer guide · v1.23.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -3601,6 +3601,7 @@ socket.onmessage = (m) =&gt; {
3601
3601
  <tr><td class="sig">pending</td><td class="desc">Applied and painted, not yet acknowledged.</td><td class="desc"><code>cell:pending</code></td></tr>
3602
3602
  <tr><td class="sig">confirmed</td><td class="desc">The server accepted it. Nothing is written back.</td><td class="desc"><code>cell:confirmed</code></td></tr>
3603
3603
  <tr><td class="sig">reverted</td><td class="desc">The server refused it; the cell is rolled back.</td><td class="desc"><code>cell:reverted</code></td></tr>
3604
+ <tr><td class="sig">conflict</td><td class="desc">The write was accepted but the server row had moved underneath it. Last-write-wins: your value stands and the server's truth is surfaced so you can reconcile it.</td><td class="desc"><code>cell:conflict</code></td></tr>
3604
3605
  <tr><td class="sig">superseded</td><td class="desc">A newer edit replaced it while it was in flight.</td><td class="desc">either, with <code>superseded: true</code></td></tr>
3605
3606
  </tbody>
3606
3607
  </table>
@@ -6137,6 +6138,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
6137
6138
  <tr><td class="name">cell:changed</td><td class="desc">A committed edit reached the data. undo distinguishes a rollback.</td></tr>
6138
6139
  <tr><td class="name">cell:clicked</td><td class="desc">A cell was clicked. Announcement only: nothing is consumed, so editing and selection behave unchanged.</td></tr>
6139
6140
  <tr><td class="name">cell:confirmed</td><td class="desc">The write reached the server.</td></tr>
6141
+ <tr><td class="name">cell:conflict</td><td class="desc">The write was accepted but the server row had moved underneath it. Last-write-wins with the divergence surfaced: your value stands and serverRow carries the server's truth.</td></tr>
6140
6142
  <tr><td class="name">cell:contextmenu</td><td class="desc">Right-click on a cell.</td></tr>
6141
6143
  <tr><td class="name">cell:dblclicked</td><td class="desc">A cell was double-clicked. Carries the row, column, value and text.</td></tr>
6142
6144
  <tr><td class="name">cell:edit:end</td><td class="desc">It closed: committed or cancelled.</td></tr>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.22.0, type declarations
2
+ * Lattice Grid 1.23.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -1141,6 +1141,68 @@ export interface OpenWrite {
1141
1141
  age: number;
1142
1142
  }
1143
1143
 
1144
+ /**
1145
+ * What an adapter can persist back to its source — the write-back capability
1146
+ * (§4.1), declared on `AdapterCapabilities.mutate`. `false` (the default) is
1147
+ * read-only by declaration; a resolved block turns every kind off unless the
1148
+ * adapter opts in.
1149
+ */
1150
+ export interface MutateCapability {
1151
+ /** The adapter can insert new rows. Wave 1: declared, not yet bridged. */
1152
+ append?: boolean;
1153
+ /** The adapter can patch existing rows. Wave 1: the wired kind (§4.3 Option A). */
1154
+ update?: boolean;
1155
+ /** The adapter can remove rows. Wave 1: declared, not yet bridged. */
1156
+ delete?: boolean;
1157
+ /**
1158
+ * The reconcile contract — what the server hands back after a successful
1159
+ * mutation (§5.1). `'row'`: the authoritative row (id, computed columns,
1160
+ * timestamps), reconciled before confirm. `'key'`: only the assigned key.
1161
+ * `'none'` (the default): nothing — the optimistic value stands
1162
+ * (last-write-wins).
1163
+ */
1164
+ returning?: 'row' | 'key' | 'none';
1165
+ }
1166
+
1167
+ /**
1168
+ * One mutation handed to `adapter.mutate(op, request)` (§4.2). Cell-scoped
1169
+ * `update` is the only kind wave 1 synthesises; `append`/`delete` are part of
1170
+ * the shape so it survives into a later structural build (card 770).
1171
+ */
1172
+ export interface MutationOp {
1173
+ kind: 'append' | 'update' | 'delete';
1174
+ /** append: the new rows (may lack a server-assigned key). */
1175
+ rows?: unknown[];
1176
+ /** update: the row key. */
1177
+ key?: string;
1178
+ /** update: the changed columns only, matching `PendingWrite` semantics. */
1179
+ patch?: Record<string, unknown>;
1180
+ /** delete: the row key(s). */
1181
+ keys?: string[];
1182
+ /** Provenance, carried through for auth / audit. */
1183
+ origin?: string;
1184
+ /** Stable id for idempotent retry / dedupe. Reserved; retry is a non-goal in wave 1. */
1185
+ requestId?: string;
1186
+ }
1187
+
1188
+ /**
1189
+ * The result of a mutation (§4.2) — the reconcile payload. A cell-update commit
1190
+ * flows this back through `PendingWrites`: `ok: false` reverts and surfaces
1191
+ * `reason`; `rows` (`returning: 'row'`) reconciles server truth before confirm;
1192
+ * `conflict` surfaces a last-write-wins divergence via `cell:conflict`.
1193
+ */
1194
+ export interface MutationResult {
1195
+ ok: boolean;
1196
+ /** `returning: 'row'` — the authoritative row(s) to reconcile to. */
1197
+ rows?: unknown[];
1198
+ /** `returning: 'key'` — server-assigned key(s) for appended rows, in order. */
1199
+ keys?: string[];
1200
+ /** On rejection — surfaced on `cell:reverted`, never swallowed. */
1201
+ reason?: string;
1202
+ /** The server's current value, for a surfaced last-write-wins conflict. */
1203
+ conflict?: { key: string; serverRow?: unknown };
1204
+ }
1205
+
1144
1206
  export interface PaginationConfig {
1145
1207
  enabled?: boolean;
1146
1208
  pageSize?: number;
@@ -2067,6 +2129,12 @@ export interface PushdownCapabilities {
2067
2129
  total?: boolean;
2068
2130
  /** Whether it can group and aggregate. */
2069
2131
  group?: boolean;
2132
+ /**
2133
+ * What the adapter can persist back — the write-back contract (§4.1). `false`
2134
+ * (the default) is read-only by declaration. A declared block opts kinds in;
2135
+ * `capabilitiesOf` resolves it to a full `MutateCapability` (or `false`).
2136
+ */
2137
+ mutate?: false | MutateCapability;
2070
2138
  }
2071
2139
 
2072
2140
  /** An engine the grid can query, and what it is able to answer. */
@@ -2077,6 +2145,13 @@ export interface PushdownAdapter {
2077
2145
  /** Run the part of the query the adapter declared it could handle. */
2078
2146
  execute(query: RemoteRequest, request?: RemoteRequest):
2079
2147
  Promise<{ rows: unknown[]; total?: number }>;
2148
+ /**
2149
+ * Persist one mutation (§4.2). Present only when `capabilities.mutate` opts
2150
+ * in. `createPushdownSource` synthesises an `edit.commit` that calls this for
2151
+ * cell updates (§4.3 Option A); `request` threads the abort signal through the
2152
+ * way `execute` receives it, and auth already lives on the adapter.
2153
+ */
2154
+ mutate?(op: MutationOp, request?: RemoteRequest): Promise<MutationResult>;
2080
2155
  }
2081
2156
 
2082
2157
  /** How one request was divided between the engine and the grid. */
@@ -2615,7 +2690,7 @@ export type EventName =
2615
2690
  | 'rows:paused' | 'rows:resumed' | 'row:received' | 'row:sent' | 'row:copied'
2616
2691
  | 'row:moved' | 'source:error' | 'stream:chunk' | 'stream:end' | 'stream:evicted'
2617
2692
  /* Cells and editing */
2618
- | 'cell:changed' | 'cell:pending' | 'cell:confirmed' | 'cell:reverted'
2693
+ | 'cell:changed' | 'cell:pending' | 'cell:confirmed' | 'cell:reverted' | 'cell:conflict'
2619
2694
  | 'cell:clicked' | 'cell:dblclicked' | 'cell:contextmenu'
2620
2695
  | 'cell:edit:start' | 'cell:edit:end' | 'row:edit:start' | 'row:edit:end'
2621
2696
  | 'row:clicked' | 'row:dblclicked'
@@ -2960,7 +3035,20 @@ export interface EditApi {
2960
3035
  changes: { key: string; colId: string; oldValue: unknown; newValue: unknown; changed: boolean }[];
2961
3036
  rejected: { key: string; colId: string; value: unknown; reason: 'permission' | 'readOnly' | 'validation' | 'locked' | 'missing' }[];
2962
3037
  };
2963
- settle(id: string, ok: boolean, reason?: string): boolean;
3038
+ /**
3039
+ * Report the outcome of an in-flight write (§18.3; §5.1-5.2 reconcile).
3040
+ *
3041
+ * `reconcile` carries server truth on a successful settle: `value` is a
3042
+ * server-authoritative value written back before `cell:confirmed`
3043
+ * (`returning: 'row'`); `conflict.serverRow` surfaces a last-write-wins
3044
+ * conflict via `cell:conflict`. Omit both to keep the optimistic value.
3045
+ */
3046
+ settle(
3047
+ id: string,
3048
+ ok: boolean,
3049
+ reason?: string,
3050
+ reconcile?: { value?: unknown; conflict?: { serverRow?: unknown } },
3051
+ ): boolean;
2964
3052
  pending(): OpenWrite[];
2965
3053
  status(key: string, colId: string): 'pending' | null;
2966
3054
  }
@@ -4024,10 +4112,18 @@ export const NO_CAPABILITIES: Readonly<Required<PushdownCapabilities>>;
4024
4112
  * per condition per query, and membership on an array is a scan. The declared
4025
4113
  * form and the resolved form differ, which is why this is its own type.
4026
4114
  */
4027
- export type ResolvedCapabilities = Omit<Required<PushdownCapabilities>, 'operators'> & {
4115
+ export type ResolvedCapabilities = Omit<Required<PushdownCapabilities>, 'operators' | 'mutate'> & {
4028
4116
  operators: ReadonlySet<string>;
4117
+ /** Resolved by `resolveMutate`: `false`, or every kind and `returning` present. */
4118
+ mutate: false | Required<MutateCapability>;
4029
4119
  };
4030
4120
 
4121
+ /**
4122
+ * Resolve an adapter's declared `mutate` block against the defaults (§4.1).
4123
+ * `false` (or anything falsy) stays `false` — read-only by declaration.
4124
+ */
4125
+ export function resolveMutate(declared?: boolean | MutateCapability): false | Required<MutateCapability>;
4126
+
4031
4127
  export function capabilitiesOf(declared?: PushdownCapabilities): ResolvedCapabilities;
4032
4128
 
4033
4129
  /**