@toclocoinc/lattice-grid 1.21.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.21.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',
@@ -5289,6 +5296,36 @@ grid.destroy();
5289
5296
  </tbody>
5290
5297
  </table>
5291
5298
  </div>
5299
+ <h3 id="type-DatasetColumnDifference">DatasetColumnDifference</h3>
5300
+ <div class="table-wrap">
5301
+ <table>
5302
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5303
+ <tbody>
5304
+ <tr><td class="name">column</td><td class="type">string</td><td class="desc">The column id, present on both grids.</td></tr>
5305
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc">The column's display name, or its id.</td></tr>
5306
+ <tr><td class="name">measure</td><td class="type">'pooledStandardMeanDifference' | 'categoricalTotalVariation'</td><td class="desc">The effect size reported for this column's family: the pooled standardised mean difference (Cohen's d) for a numeric column, the total variation of the category mix for a categorical one. Never a p-value.</td></tr>
5307
+ <tr><td class="name">magnitude</td><td class="type">number | null</td><td class="desc">The effect size in its own terms, or null when it has no scale here.</td></tr>
5308
+ <tr><td class="name">distance</td><td class="type">number</td><td class="desc">The total variation distance between the two datasets, 0 to 1 — the common scale both families reduce to, and what the ranking sorts by.</td></tr>
5309
+ <tr><td class="name">direction</td><td class="type">number</td><td class="desc">+1 when dataset A sits above dataset B, −1 below, 0 for a mix.</td></tr>
5310
+ <tr><td class="name">nA</td><td class="type">number</td><td class="desc">How many rows the first grid's side stood on.</td></tr>
5311
+ <tr><td class="name">nB</td><td class="type">number</td><td class="desc">How many rows the second grid's side stood on.</td></tr>
5312
+ <tr><td class="name">reliable</td><td class="type">boolean</td><td class="desc">False when either side is too small to read the difference from.</td></tr>
5313
+ </tbody>
5314
+ </table>
5315
+ </div>
5316
+ <h3 id="type-DatasetComparison">DatasetComparison</h3>
5317
+ <div class="table-wrap">
5318
+ <table>
5319
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5320
+ <tbody>
5321
+ <tr><td class="name">ranked</td><td class="type">DatasetColumnDifference[]</td><td class="desc">Every shared column, largest difference first.</td></tr>
5322
+ <tr><td class="name">nA</td><td class="type">number</td><td class="desc">How many rows the first grid contributed (its filtered set).</td></tr>
5323
+ <tr><td class="name">nB</td><td class="type">number</td><td class="desc">How many rows the second grid contributed (its filtered set).</td></tr>
5324
+ <tr><td class="name">unmatched</td><td class="type">{ onlyA: string[]; onlyB: string[] }</td><td class="desc">Columns present on only one side, which cannot be compared.</td></tr>
5325
+ <tr><td class="name">measures</td><td class="type">{ numeric: string; categorical: string; common: string }</td><td class="desc">The measure each family reports, and the common scale, named for a legend.</td></tr>
5326
+ </tbody>
5327
+ </table>
5328
+ </div>
5292
5329
  <h3 id="type-DataType">DataType</h3>
5293
5330
  <div class="table-wrap">
5294
5331
  <table>
@@ -5515,7 +5552,7 @@ grid.destroy();
5515
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>
5516
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>
5517
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>
5518
- <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>
5519
5556
  <tr><td class="name">pending</td><td class="type">(): OpenWrite[]</td><td class="desc"></td></tr>
5520
5557
  <tr><td class="name">status</td><td class="type">(key: string, colId: string): 'pending' | null</td><td class="desc"></td></tr>
5521
5558
  </tbody>
@@ -6229,6 +6266,49 @@ grid.destroy();
6229
6266
  </tbody>
6230
6267
  </table>
6231
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>
6232
6312
  <h3 id="type-NumberFormat">NumberFormat</h3>
6233
6313
  <div class="table-wrap">
6234
6314
  <table>
@@ -6506,6 +6586,7 @@ grid.destroy();
6506
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>
6507
6587
  <tr><td class="name">capabilities</td><td class="type">PushdownCapabilities</td><td class="desc"><small>(optional)</small></td></tr>
6508
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>
6509
6590
  </tbody>
6510
6591
  </table>
6511
6592
  </div>
@@ -6533,6 +6614,7 @@ grid.destroy();
6533
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>
6534
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>
6535
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>
6536
6618
  </tbody>
6537
6619
  </table>
6538
6620
  </div>
@@ -7048,13 +7130,14 @@ grid.destroy();
7048
7130
  <table>
7049
7131
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7050
7132
  <tbody>
7051
- <tr><td class="name">shadow</td><td class="type">(colId: string, kind: ShadowKind, rowKey: string, scope?: 'all' | 'filtered'): unknown</td><td class="desc">One shadow value for one row, by the column it shadows and the kind.</td></tr>
7133
+ <tr><td class="name">shadow</td><td class="type">(colId: string, kind: ShadowKind, rowKey: string,</td><td class="desc">One shadow value for one row, by the column it shadows and the kind. For `kind: 'specStatus'`, `spec` carries the `{lower, upper, warnLower, warnUpper}` limits to judge the row's value against; other kinds ignore it.</td></tr>
7052
7134
  <tr><td class="name">running</td><td class="type">(colId: string, kind: 'total' | 'percent', rowKey: string): number | null</td><td class="desc">A running total at one row, down the grid as it is currently ordered.</td></tr>
7053
7135
  <tr><td class="name">rebase</td><td class="type">(colId?: string): void</td><td class="desc">Make the current values the new baseline: "mark all".</td></tr>
7054
7136
  <tr><td class="name">tracking</td><td class="type">(): { columns: string[]; rows: number; forgotten: number }</td><td class="desc">What the shadow histories are costing.</td></tr>
7055
7137
  <tr><td class="name">reduce</td><td class="type">(colId: string, fn: string): unknown</td><td class="desc">Reduce a column by a named kernel over the filtered rows.</td></tr>
7056
7138
  <tr><td class="name">profile</td><td class="type">(colId: string): ColumnProfile | null</td><td class="desc">Everything worth knowing about one column, in one pass each.</td></tr>
7057
7139
  <tr><td class="name">subsetVsPopulation</td><td class="type">(opts?: { columns?: string[] }): SubsetComparison</td><td class="desc">Which columns differ most between the filtered subset and the whole population it was drawn from, ranked by effect size — never by a p-value. The measure is stated per column; a numeric and a categorical column are put on one bounded scale so they rank against each other.</td></tr>
7140
+ <tr><td class="name">datasetVsDataset</td><td class="type">(other: Grid, opts?: { columns?: string[] }): DatasetComparison</td><td class="desc">Which columns differ most between this grid and another, ranked by effect size — never by a p-value (BACKLOG-0000735). The generalisation of {@link subsetVsPopulation} from subset-vs-population to dataset-vs-dataset: two independent grids, yoked by passing one in, no shared store. A numeric column reports a pooled standardised mean difference (Cohen's d, symmetric in the two peers where Glass's delta is not); a categorical column the total variation of its category mix; both land on one bounded scale. Both sides are read over their filtered rows. Only shared columns are ranked; a column on one side alone is returned under `unmatched`.</td></tr>
7058
7141
  <tr><td class="name">correlation</td><td class="type">(a: string, b: string): number | null</td><td class="desc">Pearson's correlation between two columns.</td></tr>
7059
7142
  <tr><td class="name">covariance</td><td class="type">(a: string, b: string, opts?: { population?: boolean }): number | null</td><td class="desc">Covariance, a correlation before the scales are divided out.</td></tr>
7060
7143
  <tr><td class="name">regression</td><td class="type">(a: string, b: string): RegressionFit | null</td><td class="desc">Least-squares fit of `b` on `a`: in finance, beta and alpha.</td></tr>
@@ -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.21.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.21.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
  */
@@ -639,6 +639,17 @@ export interface Column {
639
639
  * `'filtered'` ranks within what the filters left.
640
640
  */
641
641
  scope?: 'all' | 'filtered';
642
+ /**
643
+ * For `kind: 'specStatus'`, the hard specification the row is judged
644
+ * against. `lower`/`upper` are the pass limits (a value beyond either
645
+ * fails); the optional `warnLower`/`warnUpper` are inner thresholds that
646
+ * mark a still-in-spec reading `'WARN'`. Centred-target ± tolerance is a
647
+ * deliberate follow-up and is not read here.
648
+ */
649
+ lower?: number;
650
+ upper?: number;
651
+ warnLower?: number;
652
+ warnUpper?: number;
642
653
  };
643
654
  /**
644
655
  * A running total down the grid **as it is currently ordered**.
@@ -1130,6 +1141,68 @@ export interface OpenWrite {
1130
1141
  age: number;
1131
1142
  }
1132
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
+
1133
1206
  export interface PaginationConfig {
1134
1207
  enabled?: boolean;
1135
1208
  pageSize?: number;
@@ -2056,6 +2129,12 @@ export interface PushdownCapabilities {
2056
2129
  total?: boolean;
2057
2130
  /** Whether it can group and aggregate. */
2058
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;
2059
2138
  }
2060
2139
 
2061
2140
  /** An engine the grid can query, and what it is able to answer. */
@@ -2066,6 +2145,13 @@ export interface PushdownAdapter {
2066
2145
  /** Run the part of the query the adapter declared it could handle. */
2067
2146
  execute(query: RemoteRequest, request?: RemoteRequest):
2068
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>;
2069
2155
  }
2070
2156
 
2071
2157
  /** How one request was divided between the engine and the grid. */
@@ -2210,8 +2296,13 @@ export interface PushdownSourceConfig {
2210
2296
  }
2211
2297
 
2212
2298
  export interface StatisticsApi {
2213
- /** One shadow value for one row, by the column it shadows and the kind. */
2214
- shadow(colId: string, kind: ShadowKind, rowKey: string, scope?: 'all' | 'filtered'): unknown;
2299
+ /**
2300
+ * One shadow value for one row, by the column it shadows and the kind. For
2301
+ * `kind: 'specStatus'`, `spec` carries the `{lower, upper, warnLower,
2302
+ * warnUpper}` limits to judge the row's value against; other kinds ignore it.
2303
+ */
2304
+ shadow(colId: string, kind: ShadowKind, rowKey: string,
2305
+ scope?: 'all' | 'filtered', spec?: object): unknown;
2215
2306
  /** A running total at one row, down the grid as it is currently ordered. */
2216
2307
  running(colId: string, kind: 'total' | 'percent', rowKey: string): number | null;
2217
2308
  /** Make the current values the new baseline: "mark all". */
@@ -2229,6 +2320,18 @@ export interface StatisticsApi {
2229
2320
  * on one bounded scale so they rank against each other.
2230
2321
  */
2231
2322
  subsetVsPopulation(opts?: { columns?: string[] }): SubsetComparison;
2323
+ /**
2324
+ * Which columns differ most between this grid and another, ranked by effect
2325
+ * size — never by a p-value (BACKLOG-0000735). The generalisation of
2326
+ * {@link subsetVsPopulation} from subset-vs-population to dataset-vs-dataset:
2327
+ * two independent grids, yoked by passing one in, no shared store. A numeric
2328
+ * column reports a pooled standardised mean difference (Cohen's d, symmetric
2329
+ * in the two peers where Glass's delta is not); a categorical column the total
2330
+ * variation of its category mix; both land on one bounded scale. Both sides
2331
+ * are read over their filtered rows. Only shared columns are ranked; a column
2332
+ * on one side alone is returned under `unmatched`.
2333
+ */
2334
+ datasetVsDataset(other: Grid, opts?: { columns?: string[] }): DatasetComparison;
2232
2335
  /** Pearson's correlation between two columns. */
2233
2336
  correlation(a: string, b: string): number | null;
2234
2337
  /** Covariance, a correlation before the scales are divided out. */
@@ -2336,7 +2439,18 @@ export type ShadowKind =
2336
2439
  | 'rate' | 'history' | 'firstValue' | 'streak'
2337
2440
  /** Where the row sits among the others, over every tracked row. */
2338
2441
  | 'rank' | 'rankAsc' | 'rankChange' | 'percentile' | 'quartile'
2339
- | 'zScore' | 'shareOfTotal';
2442
+ | 'zScore' | 'shareOfTotal'
2443
+ /**
2444
+ * The row's pass/fail verdict against a hard-limit spec, as a value:
2445
+ * `'PASS'`, `'WARN'` or `'FAIL'`. Sortable, filterable, groupable and
2446
+ * exportable, and rolled up by the `passRate`/`failureCount` totals. Reads
2447
+ * `{lower, upper}` (and optional inner `{warnLower, warnUpper}`) off the
2448
+ * shadow declaration; centred-target ± tolerance is a deliberate follow-up.
2449
+ */
2450
+ | 'specStatus';
2451
+
2452
+ /** The three verdicts a `specStatus` shadow can report. */
2453
+ export type SpecStatus = 'PASS' | 'WARN' | 'FAIL';
2340
2454
 
2341
2455
  export interface RegressionFit {
2342
2456
  slope: number;
@@ -2474,6 +2588,47 @@ export interface SubsetComparison {
2474
2588
  measures: { numeric: string; categorical: string; common: string };
2475
2589
  }
2476
2590
 
2591
+ export interface DatasetColumnDifference {
2592
+ /** The column id, present on both grids. */
2593
+ column: string;
2594
+ /** The column's display name, or its id. */
2595
+ name: string;
2596
+ /**
2597
+ * The effect size reported for this column's family: the pooled standardised
2598
+ * mean difference (Cohen's d) for a numeric column, the total variation of the
2599
+ * category mix for a categorical one. Never a p-value.
2600
+ */
2601
+ measure: 'pooledStandardMeanDifference' | 'categoricalTotalVariation';
2602
+ /** The effect size in its own terms, or null when it has no scale here. */
2603
+ magnitude: number | null;
2604
+ /**
2605
+ * The total variation distance between the two datasets, 0 to 1 — the common
2606
+ * scale both families reduce to, and what the ranking sorts by.
2607
+ */
2608
+ distance: number;
2609
+ /** +1 when dataset A sits above dataset B, −1 below, 0 for a mix. */
2610
+ direction: number;
2611
+ /** How many rows the first grid's side stood on. */
2612
+ nA: number;
2613
+ /** How many rows the second grid's side stood on. */
2614
+ nB: number;
2615
+ /** False when either side is too small to read the difference from. */
2616
+ reliable: boolean;
2617
+ }
2618
+
2619
+ export interface DatasetComparison {
2620
+ /** Every shared column, largest difference first. */
2621
+ ranked: DatasetColumnDifference[];
2622
+ /** How many rows the first grid contributed (its filtered set). */
2623
+ nA: number;
2624
+ /** How many rows the second grid contributed (its filtered set). */
2625
+ nB: number;
2626
+ /** Columns present on only one side, which cannot be compared. */
2627
+ unmatched: { onlyA: string[]; onlyB: string[] };
2628
+ /** The measure each family reports, and the common scale, named for a legend. */
2629
+ measures: { numeric: string; categorical: string; common: string };
2630
+ }
2631
+
2477
2632
  export interface FormattingApi {
2478
2633
  list(scope?: FormattingScope): FormattingRule[];
2479
2634
  all(): Record<FormattingScope, FormattingRule[]>;
@@ -2535,7 +2690,7 @@ export type EventName =
2535
2690
  | 'rows:paused' | 'rows:resumed' | 'row:received' | 'row:sent' | 'row:copied'
2536
2691
  | 'row:moved' | 'source:error' | 'stream:chunk' | 'stream:end' | 'stream:evicted'
2537
2692
  /* Cells and editing */
2538
- | 'cell:changed' | 'cell:pending' | 'cell:confirmed' | 'cell:reverted'
2693
+ | 'cell:changed' | 'cell:pending' | 'cell:confirmed' | 'cell:reverted' | 'cell:conflict'
2539
2694
  | 'cell:clicked' | 'cell:dblclicked' | 'cell:contextmenu'
2540
2695
  | 'cell:edit:start' | 'cell:edit:end' | 'row:edit:start' | 'row:edit:end'
2541
2696
  | 'row:clicked' | 'row:dblclicked'
@@ -2880,7 +3035,20 @@ export interface EditApi {
2880
3035
  changes: { key: string; colId: string; oldValue: unknown; newValue: unknown; changed: boolean }[];
2881
3036
  rejected: { key: string; colId: string; value: unknown; reason: 'permission' | 'readOnly' | 'validation' | 'locked' | 'missing' }[];
2882
3037
  };
2883
- 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;
2884
3052
  pending(): OpenWrite[];
2885
3053
  status(key: string, colId: string): 'pending' | null;
2886
3054
  }
@@ -3944,10 +4112,18 @@ export const NO_CAPABILITIES: Readonly<Required<PushdownCapabilities>>;
3944
4112
  * per condition per query, and membership on an array is a scan. The declared
3945
4113
  * form and the resolved form differ, which is why this is its own type.
3946
4114
  */
3947
- export type ResolvedCapabilities = Omit<Required<PushdownCapabilities>, 'operators'> & {
4115
+ export type ResolvedCapabilities = Omit<Required<PushdownCapabilities>, 'operators' | 'mutate'> & {
3948
4116
  operators: ReadonlySet<string>;
4117
+ /** Resolved by `resolveMutate`: `false`, or every kind and `returning` present. */
4118
+ mutate: false | Required<MutateCapability>;
3949
4119
  };
3950
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
+
3951
4127
  export function capabilitiesOf(declared?: PushdownCapabilities): ResolvedCapabilities;
3952
4128
 
3953
4129
  /**