@toclocoinc/lattice-grid 1.26.0 → 1.28.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/LICENSE CHANGED
@@ -27,25 +27,25 @@ and ::1 -- no watermark is shown and no key is required. Development needs no
27
27
  paperwork.
28
28
 
29
29
  --------------------------------------------------------------------------
30
- 3. WHAT REQUIRES A KEY
30
+ 3. SERVING ON OTHER HOSTS, AND WHAT A KEY REMOVES
31
31
  --------------------------------------------------------------------------
32
32
 
33
- Serving Lattice Grid from any host other than a loopback host requires a valid
34
- licence key issued by TOCLOCO Inc. This includes production, staging, internal
35
- tools, intranets and private networks. A corporate intranet is a deployment
36
- like any other; .local, .internal and private IP ranges are not exempt.
33
+ You may serve Lattice Grid from any host -- including production, staging,
34
+ internal tools, intranets and private networks -- without a licence key and
35
+ without charge. On any host other than a loopback host (section 2), the grid
36
+ renders in full and displays a trial watermark. Nothing is disabled, degraded
37
+ or withheld: every capability works, watermarked, for as long as you like.
38
+
39
+ A valid licence key issued by TOCLOCO Inc removes that watermark. Removing the
40
+ watermark is the only thing a key does (section 1), so you need one when, and
41
+ only when, you want the watermark gone -- typically for a production or
42
+ customer-facing deployment. Serving watermarked, without a key, is permitted
43
+ rather than a breach.
37
44
 
38
45
  Keys are issued per deployment rather than per developer or per seat. A key
39
- names the domains it covers and one key covers every developer, every build and
46
+ names the domains it covers, and one key covers every developer, every build and
40
47
  every user on them.
41
48
 
42
- Without a valid key, the grid renders in full and displays a trial watermark.
43
- Nothing is disabled, degraded or withheld. That behaviour exists to protect
44
- your production screens from a licence lapsing at an inconvenient moment; it is
45
- a courtesy, not a grant of licence. Deploying to a non-loopback host without a
46
- valid key is a breach of these terms whether or not the software continues to
47
- work.
48
-
49
49
  --------------------------------------------------------------------------
50
50
  4. GRANT
51
51
  --------------------------------------------------------------------------
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.26.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.28.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
package/docs/API.html CHANGED
@@ -848,8 +848,9 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
848
848
  <tbody>
849
849
  <tr><td class="name">statusBar</td><td class="type">boolean | { panels }</td><td class="desc">Composable panels along the bottom. Default set: <code>rowCount</code>, <code>selectedCount</code>, <code>aggregation</code>, <code>comments</code>, <code>updates</code>, <code>progress</code>. Each is silent when it has nothing to report.</td></tr>
850
850
  <tr><td class="name">maximise</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc"><code>false</code> removes the rail button and <code>grid.maximise</code>, for an application with its own full-screen mode.</td></tr>
851
- <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code>: nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV.</td></tr>
851
+ <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code>: nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV. <code>annotate: true</code> adds the native annotation tools — <code>pen</code>, <code>arrow</code>, <code>rect</code>, <code>highlight</code> — to the rail as toggle buttons (pressed while in use, pressed again to exit); they also appear automatically for the duration of a presentation.</td></tr>
852
852
  <tr><td class="name">groupPanel</td><td class="type">boolean | object</td><td class="desc">A drag-and-drop group-by strip above the column header — the row-group panel. Drag a heading into it to group by that column; the active groups show as removable, reorderable chips, and reordering the chips changes the nesting order. It is keyboard-operable — arrows move between chips, <code>Shift</code> with an arrow reorders, <code>Delete</code> ungroups, and an add control groups any column — and every change is announced through the live region. Off by default and non-breaking; it drives the same model as <code>grid.columns.group()</code> and reimplements nothing. The object form takes <code>hint</code>, the placeholder shown while nothing is grouped.</td></tr>
853
+ <tr><td class="name">kpis</td><td class="type">StatConfig[]</td><td class="desc">A built-in KPI/stat strip: a labelled band of stat tiles the grid places for you above the column header. Each entry is a <code>createStat</code> spec — <code>of</code>, <code>fn</code>, <code>title</code>, <code>interval</code>, <code>footer</code>, <code>format</code> and the rest, minus <code>grid</code> and <code>container</code>, which the grid supplies — so a strip tile and a hand-placed one are the same object. The tiles follow the grid's filters, recomputing on every change like a stand-alone stat does. Off by default and non-breaking; it reuses <code>createStat</code> and reimplements no compute.</td></tr>
853
854
  <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">An IANA zone every date column formats and parses in, so a grid shows one zone whatever the viewer's machine says. Individual columns may override it.</td></tr>
854
855
  <tr><td class="name">formulaFunctions</td><td class="type">object</td><td class="desc">Your own functions, added to the formula language by name. The built-in list is closed on purpose; this is the one way in, and a function you add is called exactly as a built-in is.</td></tr>
855
856
  <tr><td class="name">formatting</td><td class="type">object</td><td class="desc">Conditional formatting rules to seed, keyed by column id or <code>'*'</code>. The same shape <code>grid.formatting.all()</code> returns, so a saved view can be handed straight back.</td></tr>
@@ -1825,7 +1826,19 @@ grid.statistics.tracking(); // { columns, rows, forgotten
1825
1826
  const stats = mountPanel({ grid, panel: 'statistics', container: sidebar });
1826
1827
  stats.refresh(); // for a change the grid does not announce
1827
1828
  stats.destroy(); // yours to call: the element belongs to your page</code></pre>
1828
- <p>Any built-in panel works: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, as does a constructor of your own.</p>
1829
+ <p>Any built-in panel works: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, <code>compare</code>, <code>insights</code>, as does a constructor of your own.</p>
1830
+
1831
+ <h3>The insights panel</h3>
1832
+ <p>The <code>insights</code> tool panel is the on-screen half of the comparison analytics (§9.9): the API-only <code>subsetVsPopulation()</code>, <code>datasetVsDataset()</code>, <code>capability()</code> and <code>compareGroups()</code> rendered without you building any UI. It is opt-in — off unless you name it. Add it with <code>toolPanel: { panels: ['columns', 'insights'] }</code>.</p>
1833
+ <p>It shows four things, all over the filtered rows: the columns of the current filtered subset <strong>ranked by effect size</strong> against the whole; the same ranking against a second grid when you pass one as <code>toolPanel: { panels: ['insights'], /* config */ } </code> with <code>insights: { compareWith: otherGrid }</code>; process <strong>capability</strong> for the chosen column where it declares a <code>spec</code>; and a two-group comparison — pick a column and a column to group by, and the panel shows the <strong>named test, its confidence interval and its effect size together</strong>.</p>
1834
+ <p><strong>The stance is enforced on screen.</strong> The effect size is never shown without its interval beside it; the test or method that produced a figure is always named, from the API's own <code>method</code> string; and nothing renders a <code>significant</code> flag, a verdict, a badge or a star — the p-value is shown as the plain datum it is, when it is shown at all. The panel adds no statistic of its own: every number it shows is the one <code>grid.statistics</code> returns.</p>
1835
+ <pre><code>createGrid(el, { toolPanel: { panels: ['columns', 'insights'] } });
1836
+
1837
+ <span class="cmt">// with a second dataset to rank this grid against:</span>
1838
+ createGrid(el, {
1839
+ toolPanel: { panels: ['insights'] },
1840
+ insights: { compareWith: otherGrid },
1841
+ });</code></pre>
1829
1842
 
1830
1843
 
1831
1844
  <h3>The reductions</h3>
@@ -2627,6 +2640,7 @@ createGrid(host, { source, columns: [...] });</code></pre>
2627
2640
  <tr><td class="name">restAdapter</td><td class="desc">The API you already have</td><td class="desc">Parameter names are yours to choose. Paging and sorting are assumed; filtering is assumed absent until you declare <code>operators</code>, because an adapter that claims to filter when the endpoint ignores it returns the wrong rows silently.</td></tr>
2628
2641
  <tr><td class="name">duckdbAdapter</td><td class="desc">A DuckDB connection</td><td class="desc">Writes SQL and takes the whole query: filter tree, multi-column sort and paging. <code>from</code> is any FROM expression, so <code>read_parquet('s3://bucket/*.parquet')</code> is as valid as a table name. The engine is yours to create and install; this imports nothing, so the bundle is unchanged whether you use it or not.</td></tr>
2629
2642
  <tr><td class="name">dfqlAdapter</td><td class="desc">DemandFlow entities</td><td class="desc">Speaks <code>POST /v1/query</code>. Sends the entity, the key attribute and the prefix to match, a field projection and one field-and-term filter, matched as a case-insensitive substring. It cannot sort or page, so the grid does both, and every request carries a <code>countOnly</code> line because <code>limit</code> caps rows <em>scanned</em> rather than matched: a filtered query returns an arbitrary subset, and the count is the only thing that reveals it.</td></tr>
2643
+ <tr><td class="name">graphqlAdapter</td><td class="desc">Any GraphQL endpoint</td><td class="desc">Configured, not zero-config: GraphQL has no fixed query semantics, so you pass <code>buildQuery</code> to turn the plan into a <code>{ query, variables }</code> operation and <code>parseResponse</code> to read <code>data</code> back into rows and total. Defaults cover an offset/limit list with <code>totalCount</code> and a Relay cursor connection (<code>first</code>/<code>after</code> with <code>pageInfo</code>). The default pushes only the window and the total; declare <code>operators</code> or <code>capabilities</code> for filter/sort only alongside a <code>buildQuery</code> that emits them. A cursor connection is forward-only, so a deep window costs round trips proportional to its offset.</td></tr>
2630
2644
  </tbody>
2631
2645
  </table>
2632
2646
  </div>
@@ -2649,8 +2663,8 @@ createGrid(host, { source, columns: [...] });</code></pre>
2649
2663
  <tr><td class="name">fetch</td><td class="type">typeof fetch</td><td class="type">the global <code>fetch</code></td><td class="desc">Your own fetch, for a token that expires, a proxy, or a non-browser runtime. The adapter bundles no HTTP client. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2650
2664
  <tr><td class="name">count</td><td class="type">boolean</td><td class="type">true</td><td class="desc">Whether to ask for <code>$count=true</code> and read <code>@odata.count</code>. On by default because the grid sizes its scrollbar from the total; set <code>false</code> for a server that does not support it.</td></tr>
2651
2665
  <tr><td class="name">search</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Whether the server implements <code>$search</code>. Off by default, so quick-filter text stays with the grid until you confirm the endpoint honours it; <code>true</code> pushes it as <code>$search</code>.</td></tr>
2652
- <tr><td class="name">edit</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Opt into cell write-back. Off keeps the source read-only; <code>true</code> advertises <code>mutate: { update: true, returning: 'row' }</code>, so a committed cell edit is persisted with <code>PATCH</code>. Wave 1 wires <code>update</code> only.</td></tr>
2653
- <tr><td class="name">key</td><td class="type">string</td><td class="type">the row key</td><td class="desc">The key property a cell update names in its entity-key URL segment, e.g. <code>/Orders(&lt;key&gt;)</code>. Write-back only.</td></tr>
2666
+ <tr><td class="name">edit</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Opt into write-back. Off keeps the source read-only; <code>true</code> advertises <code>mutate: { update: true, delete: true, append: true, returning: 'row' }</code>, so a committed cell edit is persisted with <code>PATCH</code>, a row delete with <code>DELETE /EntitySet(key)</code>, and an add-row with <code>POST /EntitySet</code> reading the created entity back for its server key.</td></tr>
2667
+ <tr><td class="name">key</td><td class="type">string</td><td class="type">the row key</td><td class="desc">The key property every write addresses a row by in its entity-key URL segment, e.g. <code>/Orders(&lt;key&gt;)</code>, and that an add-row is rekeyed to from the created entity. Write-back only.</td></tr>
2654
2668
  </tbody>
2655
2669
  </table>
2656
2670
  </div>
@@ -2677,11 +2691,12 @@ createGrid(host, { source, columns: [...] });</code></pre>
2677
2691
  <tr><td class="name">encodeFilter</td><td class="type">(filters: object) =&gt; string</td><td class="type"><code>JSON.stringify</code></td><td class="desc">How the pushed condition tree becomes the <code>filter</code> parameter's value. Override it to emit whatever query language your service parses instead of JSON.</td></tr>
2678
2692
  <tr><td class="name">rows</td><td class="type">(body: unknown) =&gt; unknown[]</td><td class="type">body itself if an array, else <code>body.rows</code> then <code>body.data</code></td><td class="desc">Pulls the row array out of the response body, for an envelope that nests it somewhere else.</td></tr>
2679
2693
  <tr><td class="name">total</td><td class="type">(body: unknown, rows: unknown[]) =&gt; number</td><td class="type"><code>body.total</code> then <code>body.count</code>, else the page length</td><td class="desc">Reads the count of <em>all</em> matching rows, not the page. The grid sizes its scrollbar from it, so a page-sized total makes a large result look like one page.</td></tr>
2680
- <tr><td class="name">edit</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Opt into write-back. Off keeps the source read-only; <code>true</code> advertises <code>mutate: { update: true, delete: true, returning }</code>, so a committed cell edit is persisted with <code>PATCH</code> and a row delete with <code>DELETE</code>. Add-row needs the row-keyed pending engine and is refused loudly.</td></tr>
2681
- <tr><td class="name">returning</td><td class="type">'row' | 'none'</td><td class="type"><code>none</code></td><td class="desc">The reconcile contract for a successful write. <code>none</code> is last-write-wins — the optimistic value stands; <code>row</code> reads the server's authoritative row (via <code>writeRow</code>) back before confirm.</td></tr>
2694
+ <tr><td class="name">edit</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Opt into write-back. Off keeps the source read-only; <code>true</code> advertises <code>mutate: { update: true, delete: true, append: true, returning }</code>, so a committed cell edit is persisted with <code>PATCH</code>, a row delete with <code>DELETE</code>, and an add-row with <code>POST</code> to the collection URL.</td></tr>
2695
+ <tr><td class="name">returning</td><td class="type">'row' | 'key' | 'none'</td><td class="type"><code>none</code></td><td class="desc">The reconcile contract for a successful write. <code>none</code> is last-write-wins — the optimistic value stands; <code>row</code> reads the server's authoritative row (via <code>writeRow</code>) back before confirm; <code>key</code> reads only the server-assigned key. An add-row needs <code>row</code> or <code>key</code> so the temp row can be rekeyed.</td></tr>
2696
+ <tr><td class="name">keyField</td><td class="type">string</td><td class="type"><code>id</code></td><td class="desc">The property an add-row response carries the server-assigned key in, read back (through <code>writeRow</code>) to rekey the optimistic row. Write-back only.</td></tr>
2682
2697
  <tr><td class="name">encodeMutation</td><td class="type">(op: MutationOp) =&gt; { method: string, url: string, headers?: object, body?: unknown }</td><td class="type">the default verb map</td><td class="desc">Full control of a mutation's HTTP shape, overriding the default method, URL and body. Supersedes <code>writeUrlFor</code>.</td></tr>
2683
- <tr><td class="name">writeUrlFor</td><td class="type">(op: MutationOp) =&gt; string</td><td class="type"><code>${url}/${key}</code></td><td class="desc">The endpoint a single mutation targets, when the default per-row URL is not what the service uses. Ignored when <code>encodeMutation</code> is supplied.</td></tr>
2684
- <tr><td class="name">writeRow</td><td class="type">(body: unknown) =&gt; unknown</td><td class="type">the entity, or <code>body.row</code>/<code>body.data</code></td><td class="desc">Pulls the authoritative row out of a write response when <code>returning: 'row'</code>.</td></tr>
2698
+ <tr><td class="name">writeUrlFor</td><td class="type">(op: MutationOp) =&gt; string</td><td class="type"><code>${url}/${key}</code></td><td class="desc">The endpoint a single mutation targets, when the default per-row URL is not what the service uses. Addresses an existing row; an add-row POSTs to the collection <code>url</code> instead. Ignored when <code>encodeMutation</code> is supplied.</td></tr>
2699
+ <tr><td class="name">writeRow</td><td class="type">(body: unknown) =&gt; unknown</td><td class="type">the entity, or <code>body.row</code>/<code>body.data</code></td><td class="desc">Pulls the authoritative row out of a write response when <code>returning: 'row'</code>, and the created row an add-row reads its key from.</td></tr>
2685
2700
  </tbody>
2686
2701
  </table>
2687
2702
  </div>
@@ -2730,9 +2745,9 @@ createGrid(host, { source, columns: [...] });</code></pre>
2730
2745
  <tr><td class="name">connection</td><td class="type">object</td><td class="type">—</td><td class="desc">A live connection exposing <code>query</code>, and ideally <code>prepare</code>. Required. A connection without <code>prepare</code> is used only for unfiltered queries, because interpolating a user's filter into SQL is worse than not filtering.</td></tr>
2731
2746
  <tr><td class="name">from</td><td class="type">string</td><td class="type">—</td><td class="desc">A table, a view, or any FROM expression. Required. <code>read_parquet('s3://bucket/*.parquet')</code> is as valid as a table name.</td></tr>
2732
2747
  <tr><td class="name">fields</td><td class="type">string[]</td><td class="type">everything (<code>SELECT *</code>)</td><td class="desc">The columns to select. Name them to narrow the projection when the grid shows a subset of a wide table.</td></tr>
2733
- <tr><td class="name">writable</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Allow cell updates against a plain writable table. Off keeps the source read-only, so a <code>from</code> that is a view or an expression can never be mutated by accident. Wave 1 wires <code>update</code> only.</td></tr>
2734
- <tr><td class="name">keyField</td><td class="type">string</td><td class="type">—</td><td class="desc">The key column a cell update targets in its <code>WHERE</code>. Write-back is refused unless this names a real column, because an <code>UPDATE</code> without a unique key could touch more than one row.</td></tr>
2735
- <tr><td class="name">returning</td><td class="type">'row' | 'none'</td><td class="type"><code>row</code></td><td class="desc">The reconcile contract for a successful update. <code>row</code> appends <code>RETURNING *</code> and reconciles server truth (computed columns, triggers); <code>none</code> keeps the optimistic value.</td></tr>
2748
+ <tr><td class="name">writable</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Allow write-back against a plain writable table. Off keeps the source read-only, so a <code>from</code> that is a view or an expression can never be mutated by accident. Enables <code>update</code>, <code>delete</code> and <code>append</code>.</td></tr>
2749
+ <tr><td class="name">keyField</td><td class="type">string</td><td class="type"><code>id</code></td><td class="desc">The key column an update and a delete target in their <code>WHERE</code>, and that an add-row is rekeyed by. Write-back is refused unless this names a real column, because an <code>UPDATE</code>/<code>DELETE</code> without a unique key could touch more than one row.</td></tr>
2750
+ <tr><td class="name">returning</td><td class="type">'row' | 'none'</td><td class="type"><code>row</code></td><td class="desc">The reconcile contract for a successful write. <code>row</code> appends <code>RETURNING *</code> and reconciles server truth (computed columns, triggers); <code>none</code> keeps the optimistic value. An add-row always <code>RETURNING</code>s at least the key column regardless, since it needs that key to rekey the temp row.</td></tr>
2736
2751
  </tbody>
2737
2752
  </table>
2738
2753
  </div>
@@ -2757,6 +2772,41 @@ createGrid(host, { source, columns: [...] });</code></pre>
2757
2772
  </table>
2758
2773
  </div>
2759
2774
 
2775
+ <h5 id="graphql-options"><code>graphqlAdapter</code></h5>
2776
+ <p class="section-note">
2777
+ <strong>GraphQL has no fixed query semantics, so this adapter is configured.</strong> A filter,
2778
+ a sort and pagination are whatever the schema defines, so the two hooks are yours to write:
2779
+ <code>buildQuery</code> turns the pushed plan into the <code>{ query, variables }</code> body the
2780
+ endpoint is POSTed, and <code>parseResponse</code> reads its <code>data</code> back into
2781
+ <code>{ rows, total }</code>. The defaults cover an offset/limit list with a
2782
+ <code>totalCount</code> and a Relay cursor connection; either is replaced whole by passing the
2783
+ hook. The default query pushes only the window and asks for the total, which is why the default
2784
+ capabilities are <code>range</code> and <code>total</code> and nothing more — declare
2785
+ <code>operators</code> or <code>capabilities</code> for filter or sort only alongside a
2786
+ <code>buildQuery</code> that genuinely emits them, or the grid returns the wrong rows silently.
2787
+ </p>
2788
+ <div class="table-wrap">
2789
+ <table>
2790
+ <thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Meaning</th></tr></thead>
2791
+ <tbody>
2792
+ <tr><td class="name">url</td><td class="type">string</td><td class="type">—</td><td class="desc">The GraphQL endpoint, POSTed a <code>{ query, variables }</code> body. Required.</td></tr>
2793
+ <tr><td class="name">headers</td><td class="type">Record&lt;string, string&gt;</td><td class="type">{}</td><td class="desc">Sent on every request, merged over <code>Accept</code> and <code>Content-Type: application/json</code>. Where a fixed bearer token or API key goes. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2794
+ <tr><td class="name">fetch</td><td class="type">typeof fetch</td><td class="type">the global <code>fetch</code></td><td class="desc">Your own fetch, for an expiring token, a proxy or a non-browser runtime. The adapter bundles no HTTP client. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2795
+ <tr><td class="name">field</td><td class="type">string</td><td class="type"><code>items</code></td><td class="desc">The root query field the <em>default</em> query selects from, e.g. <code>orders</code>. Ignored when you pass <code>buildQuery</code>.</td></tr>
2796
+ <tr><td class="name">fields</td><td class="type">string[]</td><td class="type"><code>['id']</code> (with a warning)</td><td class="desc">The field names the default query's selection set requests. Name what your grid shows; a default query that selects nothing useful is surfaced rather than left an empty grid.</td></tr>
2797
+ <tr><td class="name">selection</td><td class="type">string</td><td class="type">— (uses <code>fields</code>)</td><td class="desc">A raw selection set for nested fields, e.g. <code>'id name address { city }'</code>, overriding <code>fields</code>.</td></tr>
2798
+ <tr><td class="name">pagination</td><td class="type">'offset' | 'cursor'</td><td class="type"><code>offset</code></td><td class="desc">The default convention: an <code>offset</code>/<code>limit</code> list, or a Relay <code>cursor</code> connection (<code>first</code>/<code>after</code> with <code>pageInfo</code>). A cursor connection is forward-only, so a deep window is paged forward to and costs round trips proportional to its offset.</td></tr>
2799
+ <tr><td class="name">pageSize</td><td class="type">number</td><td class="type">1000</td><td class="desc">The page size for the two forward walks: pulling the whole result (when residual work forces it) and walking a cursor connection to a window.</td></tr>
2800
+ <tr><td class="name">vars</td><td class="type">Partial&lt;Record&lt;'offset'|'limit'|'first'|'after', string&gt;&gt;</td><td class="type">{ offset:'offset', limit:'limit', first:'first', after:'after' }</td><td class="desc">Renames the pagination variables the adapter drives per page, to match the names your schema's arguments use.</td></tr>
2801
+ <tr><td class="name">capabilities</td><td class="type">PushdownCapabilities</td><td class="type">{ range:true, total:true, filter:false, sort:false, quick:false }</td><td class="desc">What your <code>buildQuery</code> actually pushes, merged over the defaults. Declaring a capability the hook does not honour returns the wrong rows silently, so the default declares only the window and the total.</td></tr>
2802
+ <tr><td class="name">operators</td><td class="type">string[]</td><td class="type">— (filtering off)</td><td class="desc">The comparisons your <code>buildQuery</code> emits, e.g. <code>['eq','gt','contains']</code>. Setting it turns filtering on as a <code>tree</code>; pair it with a <code>buildQuery</code> that translates the condition tree, or the filter is declared but not applied.</td></tr>
2803
+ <tr><td class="name">buildQuery</td><td class="type">(request: RemoteRequest) =&gt; { query, variables }</td><td class="type">the offset or cursor default</td><td class="desc">Turns the pushed plan — the window, and whatever filter/sort/quick you declared pushable — into the GraphQL operation to POST. This is where your schema's argument names live.</td></tr>
2804
+ <tr><td class="name">parseResponse</td><td class="type">(data: object) =&gt; { rows, total, pageInfo? }</td><td class="type">the offset or cursor default</td><td class="desc">Reads the operation's <code>data</code> into the row array and the count of <em>all</em> matching rows. For a cursor connection, return <code>pageInfo</code> (<code>hasNextPage</code>, <code>endCursor</code>) so the adapter can walk forward.</td></tr>
2805
+ <tr><td class="name">buildMutation</td><td class="type">(op: object) =&gt; { query, variables }</td><td class="type">— (write-back off)</td><td class="desc">Turns a mutation into a GraphQL operation. A declared follow-up wired by the write-back wave; <code>capabilities.mutate</code> stays <code>false</code> by declaration until then.</td></tr>
2806
+ </tbody>
2807
+ </table>
2808
+ </div>
2809
+
2760
2810
  <h4 id="adapter-auth">Authenticating a remote adapter</h4>
2761
2811
  <p class="section-note">
2762
2812
  Two shapes cover almost every endpoint. A <strong>fixed credential</strong> — an API key or a
@@ -3705,10 +3755,11 @@ const chart = createChart({
3705
3755
  </div>
3706
3756
 
3707
3757
  <h3>Clicking a chart</h3>
3708
- <p>A chart emits <code>point:click</code>, <code>point:hover</code> and <code>series:toggle</code>. The common use is filtering the grid from a mark, which makes the pair two views of one selection rather than a chart beside a table.</p>
3709
- <pre><code>chart.on('point:click', ({ point }) =&gt; {
3710
- grid.filters.set({ col: 'region', op: 'eq', value: point.x });
3758
+ <p>A chart emits <code>click</code>, <code>hover</code>, <code>leave</code>, <code>focus</code>, <code>draw</code>, <code>drill</code>, <code>brush</code> and <code>legend</code> — there is no <code>point:click</code>, <code>point:hover</code> or <code>series:toggle</code>. The common use is filtering the grid from a mark, which makes the pair two views of one selection rather than a chart beside a table. The <code>click</code> and <code>hover</code> payload is <strong>flat</strong> — <code>{ label, category, column, value, series, rowKeys, native, preventDefault }</code>, with no <code>point</code> wrapper — where <code>column</code> is the grid column the mark filters on and <code>category</code> the value to filter it to.</p>
3759
+ <pre><code>chart.on('click', ({ column, category }) =&gt; {
3760
+ grid.filters.set({ col: column, op: 'eq', value: category });
3711
3761
  });</code></pre>
3762
+ <p>Simplest of all, set <code>filterOnClick: true</code> in the spec and the chart applies exactly that filter itself on the clicked mark. The <code>click</code> event still fires first, so a handler that calls <code>preventDefault()</code> on the payload takes the click over instead.</p>
3712
3763
 
3713
3764
  <h3 id="chart-a-range">Chart a selected range</h3>
3714
3765
  <p>A user who drags out a block of cells — a text column and the numbers beside it — is asking a question a spreadsheet answers with one gesture: chart this. <code>chartRange</code> is that gesture. It reads the selected range, derives the chart from its shape, and returns the same live <code>Chart</code> <code>createChart</code> does, so nothing about it is a second kind of chart.</p>
@@ -4230,7 +4281,7 @@ grid.destroy();
4230
4281
  <h3 id="core-exports-example">Every core export, executed</h3>
4231
4282
  <p class="section-note">Named and resolved against the barrel on every build. A rename or a removal fails
4232
4283
  here rather than in a consumer&#39;s build.</p>
4233
- <pre data-run="js" data-expect="61" data-covers="export:AR export:AR_SA export:CS_CZ export:DA_DK export:DEFAULT_LOCALE export:DE_DE export:EL_GR export:EN_GB export:EN_US export:ES_ES export:FI_FI export:FR_CA export:FR_FR export:HU_HU export:IT_IT export:JA_JP export:LOCALES export:MESSAGE_KEYS export:NB_NO export:NL_NL export:NO_CAPABILITIES export:PL_PL export:PT_BR export:RO_RO export:SV_SE export:UK_UA export:UNIT_SYSTEMS export:applyResidual export:auditCatalogue export:compileRules export:createPushdownSource export:createRadixType export:createUnitType export:defineUnit export:dfqlAdapter export:duckdbAdapter export:evaluateFormula export:formatList export:formatUnit export:getVersion export:ingest export:ingestSync export:licenceInfo export:licenceState export:licenseInfo export:licenseState export:looksLikeFormula export:odataAdapter export:parseUnit export:planQuery export:referencesOf export:registerModules export:registerUnitSystem export:resolveCatalogue export:resolveLocale export:restAdapter export:restoreState export:serialiseState export:setLicence export:setLicense export:version"><code><span class="kw">const</span> core = <span class="kw">await</span> import('../packages/core/src/index.js');
4284
+ <pre data-run="js" data-expect="62" data-covers="export:AR export:AR_SA export:CS_CZ export:DA_DK export:DEFAULT_LOCALE export:DE_DE export:EL_GR export:EN_GB export:EN_US export:ES_ES export:FI_FI export:FR_CA export:FR_FR export:HU_HU export:IT_IT export:JA_JP export:LOCALES export:MESSAGE_KEYS export:NB_NO export:NL_NL export:NO_CAPABILITIES export:PL_PL export:PT_BR export:RO_RO export:SV_SE export:UK_UA export:UNIT_SYSTEMS export:applyResidual export:auditCatalogue export:compileRules export:createPushdownSource export:createRadixType export:createUnitType export:defineUnit export:dfqlAdapter export:duckdbAdapter export:evaluateFormula export:formatList export:formatUnit export:getVersion export:graphqlAdapter export:ingest export:ingestSync export:licenceInfo export:licenceState export:licenseInfo export:licenseState export:looksLikeFormula export:odataAdapter export:parseUnit export:planQuery export:referencesOf export:registerModules export:registerUnitSystem export:resolveCatalogue export:resolveLocale export:restAdapter export:restoreState export:serialiseState export:setLicence export:setLicense export:version"><code><span class="kw">const</span> core = <span class="kw">await</span> import('../packages/core/src/index.js');
4234
4285
 
4235
4286
  <span class="cmt">// Every declared export of the core package, named and checked. A symbol</span>
4236
4287
  <span class="cmt">// that was renamed or dropped fails here, not in a consumer's build.</span>
@@ -4245,8 +4296,8 @@ grid.destroy();
4245
4296
  'auditCatalogue', 'compileRules', 'createPushdownSource', 'createRadixType',
4246
4297
  'createUnitType', 'defineUnit', 'dfqlAdapter', 'duckdbAdapter',
4247
4298
  'evaluateFormula', 'formatList', 'formatUnit', 'getVersion',
4248
- 'ingest', 'ingestSync', 'licenceInfo', 'licenceState',
4249
- 'licenseInfo', 'licenseState', 'looksLikeFormula', 'odataAdapter',
4299
+ 'graphqlAdapter', 'ingest', 'ingestSync', 'licenceInfo',
4300
+ 'licenceState', 'licenseInfo', 'licenseState', 'looksLikeFormula', 'odataAdapter',
4250
4301
  'parseUnit', 'planQuery', 'referencesOf', 'registerModules',
4251
4302
  'registerUnitSystem', 'resolveCatalogue', 'resolveLocale', 'restAdapter',
4252
4303
  'restoreState', 'serialiseState', 'setLicence', 'setLicense',
@@ -4442,14 +4493,14 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
4442
4493
  <h3 id="remaining-config-example">The remaining option names, executed</h3>
4443
4494
  <p class="section-note">Set on a real grid and checked against its own diagnostics: an unrecognised key
4444
4495
  raises <code>config.unknown</code>, so a renamed or dropped option fails here.</p>
4445
- <pre data-run="js" data-expect="53" data-covers="config:ai config:alignedGrids config:columnDefaults config:columnGroups config:columnMenu config:columnPresets config:columnTagFilter config:comments config:components config:context config:contextMenu config:dataTypes config:detail config:diff config:edit config:environment config:facets config:formatting config:formulaFunctions config:fullWidth config:grandTotalRow config:groupPanel config:highlightOnChange config:historyBar config:hostFilter config:ingest config:licence config:pagination config:permissions config:pinnedBottomRows config:pinnedTopRows config:pipes config:pivot config:presence config:responsive config:rowClass config:rowForm config:rowStyle config:rowTemplate config:rowTransfer config:selection config:source config:state config:statusBar config:toolPanel config:totalFns config:totalOnlyChangedColumns config:tree config:typeOptions config:updates config:variants config:views config:workerUrl config:announce config:apply config:at config:background config:byKey config:compute config:config config:count config:crossFilter config:destroy config:group config:groupSelectsChildren config:groupSelectsFiltered config:height config:loaded config:order config:pageSizes config:refresh config:reload config:render config:rowLabel config:sort"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4496
+ <pre data-run="js" data-expect="54" data-covers="config:ai config:alignedGrids config:anomalySummary config:columnDefaults config:columnGroups config:columnMenu config:columnPresets config:columnTagFilter config:comments config:components config:context config:contextMenu config:dataTypes config:detail config:diff config:edit config:environment config:facets config:formatting config:formulaFunctions config:fullWidth config:grandTotalRow config:groupPanel config:highlightOnChange config:historyBar config:hostFilter config:ingest config:licence config:pagination config:permissions config:pinnedBottomRows config:pinnedTopRows config:pipes config:pivot config:presence config:responsive config:rowClass config:rowForm config:rowStyle config:rowTemplate config:rowTransfer config:selection config:source config:state config:statusBar config:toolPanel config:totalFns config:totalOnlyChangedColumns config:tree config:typeOptions config:updates config:variants config:views config:workerUrl config:announce config:apply config:at config:background config:byKey config:compute config:config config:count config:crossFilter config:destroy config:group config:groupSelectsChildren config:groupSelectsFiltered config:height config:loaded config:order config:pageSizes config:refresh config:reload config:render config:rowLabel config:sort"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4446
4497
  <span class="kw">const</span> { reportedWarnings } = <span class="kw">await</span> import('../packages/core/src/internal/util.js');
4447
4498
 
4448
4499
  <span class="cmt">// Top-level configuration keys. Each is set on a real grid, and the grid is</span>
4449
4500
  <span class="cmt">// asked whether it recognised them: an unrecognised key raises</span>
4450
4501
  <span class="cmt">// `config.unknown:&lt;key&gt;`, so a renamed or dropped option fails right here.</span>
4451
4502
  <span class="kw">const</span> documented = [
4452
- 'ai', 'alignedGrids', 'columnDefaults', 'columnGroups', 'columnMenu',
4503
+ 'ai', 'alignedGrids', 'anomalySummary', 'columnDefaults', 'columnGroups', 'columnMenu',
4453
4504
  'columnPresets', 'columnTagFilter', 'comments', 'components', 'context',
4454
4505
  'contextMenu', 'dataTypes', 'detail', 'diff', 'edit',
4455
4506
  'environment', 'facets', 'formatting', 'formulaFunctions', 'fullWidth',
@@ -4704,6 +4755,34 @@ session.push(5);
4704
4755
  session.push(15);
4705
4756
  return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')}; session ${session.reduce('avg').value}`;</code></pre>
4706
4757
 
4758
+ <h3 id="anomaly-detection">Anomaly detection</h3>
4759
+ <p class="section-note">Flag the rows that do not belong (BACKLOG-0000749). Interpretable statistics with a written-down cut, never a black box: a robust per-column outlier score, Tukey's fences, and multivariate distance from the joint centre. These are the pure kernels behind <code>grid.statistics.anomalies(...)</code> and the <code>anomalyScore</code>/<code>anomalyFlag</code> shadow columns; a host can score a plain array the same way the grid scores a column.</p>
4760
+ <div class="table-wrap">
4761
+ <table>
4762
+ <thead><tr><th>Name</th><th>Signature</th><th>Description</th></tr></thead>
4763
+ <tbody>
4764
+ <tr><td class="name">ANOMALY_METHODS</td><td class="type">readonly ('modifiedZScore' | 'iqr' | 'mahalanobis')[]</td><td class="desc">The three methods a caller may ask for, named so a result can say which produced a flag.</td></tr>
4765
+ <tr><td class="name">modifiedZScores</td><td class="type">(values, opts?) =&gt; { median, mad, threshold, scores, flags, flagged }</td><td class="desc">Per-row robust outlier score, <code>0.6745·(x − median)/MAD</code>, flagged past <code>threshold</code> (default 3.5). Built on the median and MAD, so one wild reading cannot inflate the spread and hide — the masking effect that fools an ordinary z-score. A non-finite reading and a zero-MAD column yield a null score, not an invented one.</td></tr>
4766
+ <tr><td class="name">iqrFences</td><td class="type">(values, opts?) =&gt; { q1, q3, iqr, lower, upper, k } | null</td><td class="desc">Tukey's fences, <code>[Q1 − k·IQR, Q3 + k·IQR]</code> (default <code>k = 1.5</code>), the same fence the box plot draws, on R type 7 quartiles.</td></tr>
4767
+ <tr><td class="name">mahalanobis</td><td class="type">(matrix, opts?) =&gt; { center, df, cutoff, singular, used, distances, squared, flags, flagged } | null</td><td class="desc">Distance of every row from the joint centre in the metric of the data's own covariance, cut at a χ² quantile (default the 0.975 point). Catches a row impossible only in combination — heavy <em>and</em> short — that a per-column scan misses. A row with any missing coordinate is left unplaced; a singular covariance is ridge-regularised and reported as <code>singular</code> rather than throwing.</td></tr>
4768
+ </tbody>
4769
+ </table>
4770
+ </div>
4771
+ <pre data-run="js" data-expect="methods 3; flagged 1; upper 22.875; joint true" data-covers="export:ANOMALY_METHODS export:modifiedZScores export:iqrFences export:mahalanobis"><code>const { modifiedZScores, iqrFences, mahalanobis, ANOMALY_METHODS } = await import('../packages/core/src/index.js');
4772
+ // Univariate: the robust modified z-score flags the 500 among steady readings,
4773
+ // where the mean and standard deviation an ordinary z uses would be dragged up
4774
+ // by the outlier until it no longer looked like one.
4775
+ const z = modifiedZScores([20, 21, 19, 20, 21, 19, 20, 500]);
4776
+ // Tukey's fences say the same, drawn the way a box plot draws them.
4777
+ const fence = iqrFences([20, 21, 19, 20, 21, 19, 20, 500]);
4778
+ // Multivariate: height and weight move together; the last person is tall but
4779
+ // very light, so the pair is impossible even though neither number is extreme.
4780
+ const hw = [];
4781
+ for (let i = 0; i &lt; 12; i++) hw.push([160 + i, 60 + i]);
4782
+ hw.push([182, 45]);
4783
+ const m = mahalanobis(hw); // the χ² cut flags the off-line row
4784
+ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.upper}; joint ${m.flags[12]}`;</code></pre>
4785
+
4707
4786
 
4708
4787
  <!-- BEGIN GENERATED TYPE REFERENCE -->
4709
4788
  <h2 id="type-reference">Type reference</h2>
@@ -4767,6 +4846,55 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
4767
4846
  </tbody>
4768
4847
  </table>
4769
4848
  </div>
4849
+ <h3 id="type-AnomalyReason">AnomalyReason</h3>
4850
+ <div class="table-wrap">
4851
+ <table>
4852
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
4853
+ <tbody>
4854
+ <tr><td class="name">column</td><td class="type">string</td><td class="desc">The column that put this row over the line.</td></tr>
4855
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc">The column's display name, or its id.</td></tr>
4856
+ <tr><td class="name">value</td><td class="type">number</td><td class="desc">The row's value in that column.</td></tr>
4857
+ <tr><td class="name">score</td><td class="type">number</td><td class="desc">The modified z-score, for the `modifiedZScore` method. <small>(optional)</small></td></tr>
4858
+ <tr><td class="name">lower</td><td class="type">number</td><td class="desc">The lower fence, for the `iqr` method. <small>(optional)</small></td></tr>
4859
+ <tr><td class="name">upper</td><td class="type">number</td><td class="desc">The upper fence, for the `iqr` method. <small>(optional)</small></td></tr>
4860
+ <tr><td class="name">method</td><td class="type">'modifiedZScore' | 'iqr'</td><td class="desc">Which rule flagged it. <small>(optional)</small></td></tr>
4861
+ </tbody>
4862
+ </table>
4863
+ </div>
4864
+ <h3 id="type-AnomalyReport">AnomalyReport</h3>
4865
+ <div class="table-wrap">
4866
+ <table>
4867
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
4868
+ <tbody>
4869
+ <tr><td class="name">method</td><td class="type">'modifiedZScore' | 'iqr' | 'mahalanobis'</td><td class="desc">Which rule produced the report.</td></tr>
4870
+ <tr><td class="name">k</td><td class="type">number</td><td class="desc">The IQR fence multiplier, for the `iqr` method. <small>(optional)</small></td></tr>
4871
+ <tr><td class="name">n</td><td class="type">number</td><td class="desc">How many rows the scan ran over.</td></tr>
4872
+ <tr><td class="name">rows</td><td class="type">AnomalyRow[]</td><td class="desc">The flagged rows, worst first.</td></tr>
4873
+ <tr><td class="name">flagged</td><td class="type">number</td><td class="desc">How many rows were flagged.</td></tr>
4874
+ <tr><td class="name">skipped</td><td class="type">string[]</td><td class="desc">The column ids that were not numeric and so could not be scored.</td></tr>
4875
+ <tr><td class="name">scored</td><td class="type">number</td><td class="desc">How many numeric columns were scored (univariate). <small>(optional)</small></td></tr>
4876
+ <tr><td class="name">columns</td><td class="type">unknown</td><td class="desc">Per-column summaries (univariate): the centre, spread and fence per column. <small>(optional)</small></td></tr>
4877
+ <tr><td class="name">df</td><td class="type">number</td><td class="desc">The degrees of freedom of the χ² cut (multivariate). <small>(optional)</small></td></tr>
4878
+ <tr><td class="name">cutoff</td><td class="type">number | null</td><td class="desc">The χ² cut the squared distance is compared against (multivariate). <small>(optional)</small></td></tr>
4879
+ <tr><td class="name">center</td><td class="type">number[]</td><td class="desc">The joint centre the distances are measured from (multivariate). <small>(optional)</small></td></tr>
4880
+ <tr><td class="name">used</td><td class="type">number</td><td class="desc">How many complete rows defined the metric (multivariate). <small>(optional)</small></td></tr>
4881
+ <tr><td class="name">singular</td><td class="type">boolean</td><td class="desc">Whether the covariance was singular and had to be regularised (multivariate). <small>(optional)</small></td></tr>
4882
+ </tbody>
4883
+ </table>
4884
+ </div>
4885
+ <h3 id="type-AnomalyRow">AnomalyRow</h3>
4886
+ <div class="table-wrap">
4887
+ <table>
4888
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
4889
+ <tbody>
4890
+ <tr><td class="name">rowKey</td><td class="type">string | null</td><td class="desc">The row key — stable across a sort or a feed, where the index is not.</td></tr>
4891
+ <tr><td class="name">index</td><td class="type">number</td><td class="desc">The physical row index at the time of the call.</td></tr>
4892
+ <tr><td class="name">score</td><td class="type">number | null</td><td class="desc">The row's headline score: its most extreme modified z-score across the flagging columns (univariate), the Mahalanobis distance (multivariate), or null for the IQR method, which has no single score.</td></tr>
4893
+ <tr><td class="name">squared</td><td class="type">number | null</td><td class="desc">The squared Mahalanobis distance, for the `mahalanobis` method. <small>(optional)</small></td></tr>
4894
+ <tr><td class="name">why</td><td class="type">AnomalyReason[]</td><td class="desc">Why this row was flagged: the columns and how far, so it is explainable.</td></tr>
4895
+ </tbody>
4896
+ </table>
4897
+ </div>
4770
4898
  <h3 id="type-ApproximateEntry">ApproximateEntry</h3>
4771
4899
  <p class="section-note">One entry of the approximate maintenance tier.</p>
4772
4900
  <div class="table-wrap">
@@ -5792,6 +5920,8 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
5792
5920
  <tr><td class="name">undo</td><td class="type">(): void</td><td class="desc"></td></tr>
5793
5921
  <tr><td class="name">redo</td><td class="type">(): void</td><td class="desc"></td></tr>
5794
5922
  <tr><td class="name">setCells</td><td class="type">(writes: { key: string; colId: string; value: unknown }[], type?: 'cell' | 'fill' | 'paste'): number</td><td class="desc"></td></tr>
5923
+ <tr><td class="name">bulkSet</td><td class="type">(value: unknown, opts?: { cells?: { key: string; colId: string }[] }): number</td><td class="desc">Set one value across a block of cells as a single undoable step (§12, card 740). Defaults to the selected range; read-only and non-editable cells are skipped and every write runs the normal parse/validate path.</td></tr>
5924
+ <tr><td class="name">fill</td><td class="type">(opts?: { direction?: 'down' | 'up' | 'left' | 'right'; series?: boolean; range?: CellRange }): number</td><td class="desc">Fill a selected range from its leading edge as one undoable step (§12, card 740). The default copies the anchor across the range (Excel's Ctrl+D and its natural siblings); `series: true` extrapolates a numeric or date series from the first one or two cells of each line, falling back to a copy for types with no series. `direction` defaults to `'down'`.</td></tr>
5795
5925
  <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>
5796
5926
  <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>
5797
5927
  <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>
@@ -6220,6 +6350,7 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
6220
6350
  <tr><td class="name">cornerRadius</td><td class="type">boolean | number | string</td><td class="desc">Round the grid's outer corners. Square by default. `true` adopts the theme's own radius; a number is pixels; a string is used as written, so a host can pass its own token or a relative unit. <small>(optional)</small></td></tr>
6221
6351
  <tr><td class="name">stripedRows</td><td class="type">boolean</td><td class="desc">Shade alternate data rows (zebra striping). Off by default, and strictly opt-in: an existing grid must look exactly the same on upgrade. When `true`, every other data row takes the theme's `--lattice-surface-alt` background, which every palette already defines, so dark, high-contrast and terminal stripe correctly without extra work. Parity follows the row's *logical* index, not its position in the DOM, so a row keeps its stripe across a scroll even though the rows are recycled. Structural rows — group headings, group footers and the grand total — are never striped, and both selection and hover still win over the stripe. <small>(optional)</small></td></tr>
6222
6352
  <tr><td class="name">columnTagFilter</td><td class="type">boolean | { multiple?: boolean; label?: string }</td><td class="desc">Show a bar above the column headings for filtering columns by tag. Off by default, and it draws nothing unless some column carries a `tags` entry. `multiple: true` lets more than one tag be chosen at once. Only tagged columns are ever hidden, so an untagged account or total column stays visible whatever is selected. <small>(optional)</small></td></tr>
6353
+ <tr><td class="name">anomalySummary</td><td class="type">boolean | { column?: string; label?: string }</td><td class="desc">Show a small chip in the grid chrome that reads how many rows an anomaly shadow column has flagged, and filters the grid to exactly those when it is clicked (BACKLOG-0000799). Off by default, and it draws nothing unless a column declares a `shadow: { kind: 'anomalyFlag' }`. The count and the filter both read that one shadow column, so the number on the chip is the number of rows the click reveals. `column` names the base column to summarise when more than one anomaly-flag shadow is present; `label` overrides the chip's wording. <small>(optional)</small></td></tr>
6223
6354
  <tr><td class="name">typeOptions</td><td class="type">Record&lt;string, {</td><td class="desc">Per-column options a data type reads. `ratio` and `percentRate` use `{ weight }` to name the column their average is weighted by. A unit type reads `{ significantFigures }` to render to a fixed precision rather than a fixed number of decimals. <small>(optional)</small></td></tr>
6224
6355
  <tr><td class="name">rowTemplate</td><td class="type">string | {</td><td class="desc"><small>(optional)</small></td></tr>
6225
6356
  <tr><td class="name">gallery</td><td class="type">boolean | {</td><td class="desc">Present rows as a gallery of tiles (§7.12). The tiled card layout with a size-driven column count: tiles as wide as `tileWidth` allows, as many across as the container holds, laid out by the same 2-D virtualisation the grid already runs. `true` draws a tile per row generated from the columns; an object sizes them or supplies a template. Presentation only — sort, filter, group and the data pipeline are unchanged. <small>(optional)</small></td></tr>
@@ -6276,6 +6407,7 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
6276
6407
  <tr><td class="name">rowStyle</td><td class="type">CellStyle | ((p: RowStyleParams) =&gt; CellStyle)</td><td class="desc">Inline styles for every row. Camel-case or hyphenated property names. <small>(optional)</small></td></tr>
6277
6408
  <tr><td class="name">toolPanel</td><td class="type">boolean | {</td><td class="desc"><small>(optional)</small></td></tr>
6278
6409
  <tr><td class="name">groupPanel</td><td class="type">boolean | {</td><td class="desc">A drag-and-drop group-by strip above the column header — the pattern AG Grid calls the row-group panel. Drag a column heading into it to group by that column; the active groups show as removable, reorderable chips, and reordering the chips changes the nesting order. It is keyboard-operable (arrows navigate, Shift+arrow reorders, Delete ungroups, and an add control groups any column), and every change is announced through the live region, which is why it also addresses the drag-only complaint of BACKLOG-0000429. Off by default and non-breaking, matching `toolPanel`. It drives the same grouping model as `grid.columns.group()`; it reimplements nothing. <small>(optional)</small></td></tr>
6410
+ <tr><td class="name">kpis</td><td class="type">Array&lt;Omit&lt;StatConfig, 'grid' | 'container'&gt;&gt;</td><td class="desc">A built-in KPI/stat strip: a labelled band of {@link createStat} tiles the grid places for you, above the column header. Each entry is a stat spec — the same fields {@link StatConfig} takes, minus `grid` and `container`, which the grid supplies — so a strip tile and a hand-placed one are the same object. The tiles follow the grid's filters, recomputing on every change exactly as a stand-alone stat does. Off by default and non-breaking, matching `groupPanel`: no `kpis` means no band and no cost. It reuses `createStat` and reimplements no compute. <small>(optional)</small></td></tr>
6279
6411
  <tr><td class="name">quickFilterText</td><td class="type">string</td><td class="desc">The quick filter's initial text. <small>(optional)</small></td></tr>
6280
6412
  <tr><td class="name">permissions</td><td class="type">PermissionPolicy</td><td class="desc">Per-column read/write/hidden policy. A usability control, not a security boundary: hidden data is still resident in the store. Enforce the same policy server-side with `permittedColumns` / `permittedExport`. <small>(optional)</small></td></tr>
6281
6413
  <tr><td class="name">diff</td><td class="type">{</td><td class="desc">Prior state for diff and audit mode. <small>(optional)</small></td></tr>
@@ -6331,6 +6463,55 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
6331
6463
  </tbody>
6332
6464
  </table>
6333
6465
  </div>
6466
+ <h3 id="type-GroupComparison">GroupComparison</h3>
6467
+ <p class="section-note">The result of {@link StatisticsApi.compareGroups}: how big *and* how sure, as data to interpret. Carries no significance verdict — the p-value is a number, never a flag or a badge.</p>
6468
+ <div class="table-wrap">
6469
+ <table>
6470
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6471
+ <tbody>
6472
+ <tr><td class="name">test</td><td class="type">'welch' | 'mannWhitney' | 'chiSquare'</td><td class="desc">The test used, named so it is never hidden.</td></tr>
6473
+ <tr><td class="name">chosenBy</td><td class="type">'auto' | 'override'</td><td class="desc">Whether the test was chosen automatically or forced by the caller.</td></tr>
6474
+ <tr><td class="name">reason</td><td class="type">string</td><td class="desc">Why this test — the column family, a normality screen, or the override.</td></tr>
6475
+ <tr><td class="name">statistic</td><td class="type">number</td><td class="desc">The test statistic.</td></tr>
6476
+ <tr><td class="name">statisticName</td><td class="type">string</td><td class="desc">What the statistic is: `t`, `U`, or `chiSquare`.</td></tr>
6477
+ <tr><td class="name">df</td><td class="type">number | null</td><td class="desc">The degrees of freedom, where the test has them; null for Mann-Whitney.</td></tr>
6478
+ <tr><td class="name">pValue</td><td class="type">number</td><td class="desc">The two-sided p-value, returned as data for the caller to interpret. Never thresholded into a verdict here.</td></tr>
6479
+ <tr><td class="name">interval</td><td class="type">GroupDifferenceInterval | null</td><td class="desc">The confidence interval on the difference, or null when there is none.</td></tr>
6480
+ <tr><td class="name">effectSize</td><td class="type">GroupEffectSize</td><td class="desc">The paired effect size, so the p-value is never read on its own.</td></tr>
6481
+ <tr><td class="name">nA</td><td class="type">number</td><td class="desc">How many rows the first group stood on.</td></tr>
6482
+ <tr><td class="name">nB</td><td class="type">number</td><td class="desc">How many rows the second group stood on.</td></tr>
6483
+ <tr><td class="name">groups</td><td class="type">[unknown, unknown]</td><td class="desc">The two group values compared, as keys.</td></tr>
6484
+ <tr><td class="name">reliable</td><td class="type">boolean</td><td class="desc">False when either group is under the reliability floor.</td></tr>
6485
+ <tr><td class="name">method</td><td class="type">string</td><td class="desc">The test's method, named per the reference-suite honesty rule.</td></tr>
6486
+ </tbody>
6487
+ </table>
6488
+ </div>
6489
+ <h3 id="type-GroupDifferenceInterval">GroupDifferenceInterval</h3>
6490
+ <p class="section-note">A confidence interval on the difference a two-sample test measured.</p>
6491
+ <div class="table-wrap">
6492
+ <table>
6493
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6494
+ <tbody>
6495
+ <tr><td class="name">estimate</td><td class="type">number</td><td class="desc">The point estimate of the difference the interval is around.</td></tr>
6496
+ <tr><td class="name">lower</td><td class="type">number</td><td class="desc"></td></tr>
6497
+ <tr><td class="name">upper</td><td class="type">number</td><td class="desc"></td></tr>
6498
+ <tr><td class="name">confidence</td><td class="type">number</td><td class="desc">The level the bounds were computed at, 0 to 1.</td></tr>
6499
+ <tr><td class="name">method</td><td class="type">string</td><td class="desc">The method, named for honesty: `welch-t`, `hodges-lehmann`, `newcombe`.</td></tr>
6500
+ <tr><td class="name">category</td><td class="type">unknown</td><td class="desc">For a chi-square interval, which category's share the difference is of. <small>(optional)</small></td></tr>
6501
+ </tbody>
6502
+ </table>
6503
+ </div>
6504
+ <h3 id="type-GroupEffectSize">GroupEffectSize</h3>
6505
+ <p class="section-note">The effect size paired with a two-sample test — the "how big" half.</p>
6506
+ <div class="table-wrap">
6507
+ <table>
6508
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6509
+ <tbody>
6510
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc">The named measure: `pooledStandardMeanDifference` (Cohen's d) for the numeric tests, `categoricalTotalVariation` for chi-square.</td></tr>
6511
+ <tr><td class="name">value</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>
6512
+ </tbody>
6513
+ </table>
6514
+ </div>
6334
6515
  <h3 id="type-HighlightApi">HighlightApi</h3>
6335
6516
  <div class="table-wrap">
6336
6517
  <table>
@@ -6958,6 +7139,7 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
6958
7139
  <tr><td class="name">icon</td><td class="type">string | (() =&gt; string)</td><td class="desc"><small>(optional)</small></td></tr>
6959
7140
  <tr><td class="name">run</td><td class="type">(params: RailActionParams): void</td><td class="desc"></td></tr>
6960
7141
  <tr><td class="name">enabled</td><td class="type">(): boolean</td><td class="desc"><small>(optional)</small></td></tr>
7142
+ <tr><td class="name">active</td><td class="type">(): boolean</td><td class="desc">Marks the action as a toggle and reports whether it is currently on. When present the rail renders `aria-pressed` and a pressed style, re-read on every repaint; a one-shot action omits it and is unchanged. This is the hook the native annotation tools use, and it is available to a host button that is itself a toggle. <small>(optional)</small></td></tr>
6961
7143
  </tbody>
6962
7144
  </table>
6963
7145
  </div>
@@ -7425,8 +7607,10 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
7425
7607
  <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>
7426
7608
  <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>
7427
7609
  <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>
7610
+ <tr><td class="name">anomalies</td><td class="type">(opts?: { columns?: string[]</td><td class="desc">The rows that do not belong (BACKLOG-0000749): anomaly detection over the filtered rows by the robust modified z-score (`modifiedZScore`, the default), Tukey's IQR fences (`iqr`), or multivariate Mahalanobis distance over the chosen columns (`mahalanobis`). Every flagged row carries the score behind it and the reason for it, so a flag is explainable rather than a verdict from nowhere. Non-numeric columns are returned under `skipped`.</td></tr>
7428
7611
  <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>
7429
7612
  <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>
7613
+ <tr><td class="name">compareGroups</td><td class="type">(colId: string, opts: TwoSampleSpec): GroupComparison | null</td><td class="desc">Is the difference between two groups real? A two-sample test returned as data to interpret — never a verdict (BACKLOG-0000750). The significance boundary the comparison story (653, 735) stopped short of: those rank by how *much* columns differ and return no p-value; this answers *how sure* for one chosen pair of groups and hands the p-value back as data. There is no `significant` flag, no badge, and no multiple-comparison correction. The rows are split by `opts.by`, the test is chosen by the column's family and named in the result (overridable with `opts.test`): Welch's t or Mann-Whitney U for a numeric column, chi-square for a categorical one. Every result pairs a confidence interval on the difference with the effect size, so it is always "how big and how sure".</td></tr>
7430
7614
  <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>
7431
7615
  <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>
7432
7616
  <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>
@@ -7532,6 +7716,20 @@ return `avg ${avg.value} over ${avg.over.size}; kinds ${WINDOW_KINDS.join('/')};
7532
7716
  </tbody>
7533
7717
  </table>
7534
7718
  </div>
7719
+ <h3 id="type-TwoSampleSpec">TwoSampleSpec</h3>
7720
+ <p class="section-note">How {@link StatisticsApi.compareGroups} splits the rows and picks a test.</p>
7721
+ <div class="table-wrap">
7722
+ <table>
7723
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7724
+ <tbody>
7725
+ <tr><td class="name">by</td><td class="type">string</td><td class="desc">The column whose values split the rows into groups. Required.</td></tr>
7726
+ <tr><td class="name">groups</td><td class="type">[unknown, unknown]</td><td class="desc">The two group values to compare. The two most frequent when omitted. <small>(optional)</small></td></tr>
7727
+ <tr><td class="name">test</td><td class="type">'auto' | 'welch' | 'mannWhitney' | 'chiSquare'</td><td class="desc">Force a test rather than choosing by column family. `auto` (the default) picks Welch or Mann-Whitney for a numeric column and chi-square for a categorical one; the choice is always named in the result. <small>(optional)</small></td></tr>
7728
+ <tr><td class="name">confidence</td><td class="type">number</td><td class="desc">The confidence level for the interval, 0 to 1. 0.95 by default. <small>(optional)</small></td></tr>
7729
+ <tr><td class="name">category</td><td class="type">unknown</td><td class="desc">The focal category for a chi-square difference interval, when the column has more than two categories. Without it, a multi-category comparison reports no scalar interval, only the effect size. <small>(optional)</small></td></tr>
7730
+ </tbody>
7731
+ </table>
7732
+ </div>
7535
7733
  <h3 id="type-UnitConfig">UnitConfig</h3>
7536
7734
  <p class="section-note">How a column stores, parses and renders a quantity.</p>
7537
7735
  <div class="table-wrap">