@toclocoinc/lattice-grid 1.39.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/docs/API.html +145 -0
  3. package/docs/api-detail.html +2 -1
  4. package/lattice-grid.d.ts +255 -1
  5. package/lattice-grid.esm.min.js +849 -136
  6. package/lattice-grid.min.cjs +847 -136
  7. package/lattice-grid.min.js +847 -136
  8. package/modules/ai.esm.min.js +1349 -0
  9. package/modules/ai.min.cjs +1352 -0
  10. package/modules/ai.min.js +1352 -0
  11. package/modules/angular.esm.min.js +2 -2
  12. package/modules/angular.min.cjs +2 -2
  13. package/modules/angular.min.js +2 -2
  14. package/modules/chart-alluvial.esm.min.js +1 -1
  15. package/modules/chart-arc.esm.min.js +1 -1
  16. package/modules/chart-bubblemap.esm.min.js +1 -1
  17. package/modules/chart-bump.esm.min.js +1 -1
  18. package/modules/chart-calendar.esm.min.js +1 -1
  19. package/modules/chart-decomposition.esm.min.js +1 -1
  20. package/modules/chart-diverging.esm.min.js +1 -1
  21. package/modules/chart-dumbbell.esm.min.js +1 -1
  22. package/modules/chart-fan.esm.min.js +1 -1
  23. package/modules/chart-hexbin.esm.min.js +1 -1
  24. package/modules/chart-hexmap.esm.min.js +1 -1
  25. package/modules/chart-icicle.esm.min.js +1 -1
  26. package/modules/chart-parallel.esm.min.js +1 -1
  27. package/modules/chart-ridgeline.esm.min.js +1 -1
  28. package/modules/chart-roc.esm.min.js +1 -1
  29. package/modules/chart-slope.esm.min.js +1 -1
  30. package/modules/chart-splom.esm.min.js +1 -1
  31. package/modules/chart-waffle.esm.min.js +1 -1
  32. package/modules/charts.esm.min.js +4 -4
  33. package/modules/charts.min.cjs +4 -4
  34. package/modules/charts.min.js +4 -4
  35. package/modules/data-router.esm.min.js +212 -6
  36. package/modules/data-router.min.cjs +212 -6
  37. package/modules/data-router.min.js +212 -6
  38. package/modules/devtools.esm.min.js +2 -2
  39. package/modules/devtools.min.cjs +2 -2
  40. package/modules/devtools.min.js +2 -2
  41. package/modules/dhtmlx-compat.esm.min.js +4 -4
  42. package/modules/dhtmlx-compat.min.cjs +4 -4
  43. package/modules/dhtmlx-compat.min.js +4 -4
  44. package/modules/gantt.esm.min.js +4 -4
  45. package/modules/gantt.min.cjs +4 -4
  46. package/modules/gantt.min.js +4 -4
  47. package/modules/htmx.esm.min.js +845 -136
  48. package/modules/htmx.min.cjs +845 -136
  49. package/modules/htmx.min.js +845 -136
  50. package/modules/kanban.esm.min.js +350 -7
  51. package/modules/kanban.min.cjs +350 -7
  52. package/modules/kanban.min.js +350 -7
  53. package/modules/kpi.esm.min.js +4 -4
  54. package/modules/kpi.min.cjs +4 -4
  55. package/modules/kpi.min.js +4 -4
  56. package/modules/mock-socket.esm.min.js +2 -2
  57. package/modules/mock-socket.min.cjs +2 -2
  58. package/modules/mock-socket.min.js +2 -2
  59. package/modules/react.esm.min.js +2 -2
  60. package/modules/react.min.cjs +2 -2
  61. package/modules/react.min.js +2 -2
  62. package/modules/svelte.esm.min.js +2 -2
  63. package/modules/svelte.min.cjs +2 -2
  64. package/modules/svelte.min.js +2 -2
  65. package/modules/vue.esm.min.js +2 -2
  66. package/modules/vue.min.cjs +2 -2
  67. package/modules/vue.min.js +2 -2
  68. package/modules/webcomponent.esm.min.js +847 -136
  69. package/modules/webcomponent.min.cjs +847 -136
  70. package/modules/webcomponent.min.js +847 -136
  71. package/package.json +1 -1
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.39.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.42.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
package/docs/API.html CHANGED
@@ -5268,6 +5268,66 @@ router.apply([{ op: 'delete', row: { id: 'd2' } }]); <span class="c
5268
5268
  router.destroy();
5269
5269
  <span class="kw">return</span> [seeded, afterRemove, band].join(' | ');</code></pre>
5270
5270
 
5271
+ <h2 id="ai">The AI narrative / insights layer</h2>
5272
+ <p><code>modules/ai</code> is an opt-in layer that produces a short, plain-language <strong>narrative</strong> of the grid's <em>computed</em> figures &mdash; a per-KPI / per-chart / per-column &ldquo;Explain&rdquo;, or an insights panel over the current (filtered) view. It is a separate bundle that adds no weight to a page that does not load it, changes nothing in grid core, and pulls in no dependency. <strong>The grid makes no AI call of its own:</strong> <code>createAI</code> never imports a provider SDK, never reads a key, and never makes a network request. It calls one async callback you supply, <code>ask()</code> &mdash; your model, your key, your privacy decision &mdash; exactly the philosophy of the data adapters, where auth and transport are always the caller's.</p>
5273
+ <pre><code>import { createAI } from '@toclocoinc/lattice-grid/modules/ai';
5274
+
5275
+ const ai = createAI(grid, {
5276
+ ask: async ({ system, messages, tools, schema, signal }) =&gt; {
5277
+ const r = await myProvider.chat({ system, messages, tools, signal });
5278
+ return { text: r.text, toolCalls: r.toolCalls }; <span class="cmt">// or a bare string</span>
5279
+ },
5280
+ maxRows: 50, <span class="cmt">// cap what any tool result carries to ask()</span>
5281
+ redact: ['ssn', 'salary'], <span class="cmt">// columns whose values never leave the browser</span>
5282
+ });
5283
+
5284
+ ai.insights(document.querySelector('#insights')); <span class="cmt">// the panel</span>
5285
+ const { text, flagged } = await ai.explain({ kind: 'column', colId: 'amount' });</code></pre>
5286
+ <p><strong>Grounded, and reconciled.</strong> Where your provider offers tool-use, the model is given a curated <strong>read-only</strong> tool set (<code>getSchema</code>, <code>getProfile</code>, <code>getStatistics</code>, <code>getForecast</code>, <code>runQuery</code>) and <em>our</em> engine computes what it asks for; where it does not, the module builds a <strong>facts packet</strong> from the grid's computed results (<code>grid.statistics</code>, the profile, forecasts, view counts) and passes it in the prompt. Either way, <strong>every figure in the narrative is reconciled against the values the engine produced this render</strong> &mdash; an ungrounded number is stripped before the user sees it. The prompt is constrained to narrate-only; the layer is read-only and never mutates data. <code>redact</code> and <code>maxRows</code> bound what the module hands <code>ask()</code>, and the module sends nothing itself. An <code>ask()</code> error surfaces a friendly message and the grid stays fully usable &mdash; AI is additive, never load-bearing.</p>
5287
+ <p><code>createAI</code> is complementary to <code>grid.ai</code>: <code>grid.ai</code> is the intent/plan skill layer (a question becomes a validated filter/sort plan you preview and apply); <code>createAI</code> is the narrative/insights consumer that explains figures. They can share one host <code>ask()</code> &mdash; pass none to <code>createAI</code> and it adopts the grid's configured <code>ai.ask</code>, running the facts-packet path over it.</p>
5288
+ <div class="table-wrap">
5289
+ <table>
5290
+ <thead><tr><th>Member</th><th>Description</th></tr></thead>
5291
+ <tbody>
5292
+ <tr><td class="sig">createAI(grid, config)</td><td class="desc">Create an AI narrative / insights controller over a live grid (headless or rendered). Config: <code>ask</code> (the host callback; falls back to the grid's <code>ai.ask</code>), <code>enable</code>, <code>maxRows</code>, <code>redact</code>, <code>tools</code>, <code>locale</code>, <code>reconcile</code> (<code>'strip'</code>/<code>'flag'</code>), <code>element</code>, <code>onNarrative</code>, <code>onError</code>.</td></tr>
5293
+ <tr><td class="sig">explain(target?, opts?) / narrate(...)</td><td class="desc">Produce a grounded, reconciled narrative. <code>target</code> is <code>{ kind: 'view' }</code>, <code>{ kind: 'column', colId }</code>, <code>{ kind: 'forecast', colId, options }</code>, or <code>{ kind: 'kpi'|'chart', facts }</code>. Resolves to <code>{ text, facts, grounded, flagged, rounds, mode }</code>.</td></tr>
5294
+ <tr><td class="sig">insights(el?, opts?)</td><td class="desc">Mount (or re-target) the insights panel into an element, its generate control wired to a view narrative. Keeps the grid usable on an <code>ask()</code> error.</td></tr>
5295
+ <tr><td class="sig">attachExplain(target, opts?)</td><td class="desc">Build an &ldquo;Explain&rdquo; button bound to a target (a KPI tile, a chart datum, a column). Clicking it narrates the target.</td></tr>
5296
+ <tr><td class="sig">facts(target?, opts?)</td><td class="desc">Build the facts packet for a target <em>without</em> calling <code>ask()</code> &mdash; the exact grounded set a narrative would use, and what would leave the browser.</td></tr>
5297
+ <tr><td class="sig">on(name, fn) / off(name, fn) / destroy()</td><td class="desc">Events: <code>narrative</code> and <code>error</code>. <code>destroy()</code> tears the controller down and leaves the grid untouched.</td></tr>
5298
+ </tbody>
5299
+ </table>
5300
+ </div>
5301
+ <h3 id="ai-reconcile-example">Grounded, with the reconciliation guard, executed</h3>
5302
+ <p class="section-note">A mock <code>ask()</code> returns one grounded figure (the row count) and one invented one.
5303
+ The number-reconciliation guard strips the ungrounded figure and keeps the grounded one. Run headless on every build.</p>
5304
+ <pre data-run="js" data-expect="1 flagged | true kept | true stripped" data-covers="export:createAI"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
5305
+ <span class="kw">const</span> { createAI } = <span class="kw">await</span> import('../packages/modules/ai/index.js');
5306
+
5307
+ <span class="kw">const</span> grid = createHeadlessGrid({
5308
+ rowKey: 'id',
5309
+ columns: [{ field: 'id' }, { field: 'region' }, { field: 'amount', type: 'number' }],
5310
+ rows: [
5311
+ { id: 1, region: 'EMEA', amount: 100 },
5312
+ { id: 2, region: 'AMER', amount: 300 },
5313
+ { id: 3, region: 'APAC', amount: 200 },
5314
+ ],
5315
+ });
5316
+
5317
+ <span class="cmt">// Your model, your key. The grid makes no network call — it awaits this.</span>
5318
+ <span class="cmt">// This mock returns one grounded figure (3 rows) and one invented one (900%).</span>
5319
+ <span class="kw">const</span> ask = <span class="kw">async</span> () =&gt; ({ text: 'There are 3 rows in view. Confidence 900%.' });
5320
+
5321
+ <span class="kw">const</span> ai = createAI(grid, { ask, tools: <span class="kw">false</span> });
5322
+ <span class="kw">const</span> result = <span class="kw">await</span> ai.explain({ kind: 'view' });
5323
+
5324
+ <span class="kw">const</span> kept = result.text.includes('3 rows'); <span class="cmt">// grounded — survives</span>
5325
+ <span class="kw">const</span> stripped = !result.text.includes('900%'); <span class="cmt">// hallucinated — removed</span>
5326
+
5327
+ ai.destroy();
5328
+ grid.destroy();
5329
+ <span class="kw">return</span> `${result.flagged.length} flagged | ${kept} kept | ${stripped} stripped`;</code></pre>
5330
+
5271
5331
  <h2 id="mocksocket">The mock socket</h2>
5272
5332
  <p><code>modules/mock-socket</code> is a serverless stand-in for a live <code>WebSocket</code> feed, for building and demonstrating a real-time UI with <strong>no backend</strong>. <code>MockWebSocket</code> presents the same surface as the browser's <code>WebSocket</code> &mdash; the same <code>readyState</code> and state constants, the same <code>onopen</code>, <code>onmessage</code>, <code>onclose</code> and <code>onerror</code>, <code>addEventListener</code>, <code>send</code> and <code>close</code> &mdash; so the code that reads it does not change when it is swapped for a real one. It fires an initial snapshot the moment it opens, then a stream of deltas on a timer, all from a generator you hand it. It is a dev and test utility: optional, imports nothing from the grid, and is never pulled into the core bundle. It pairs naturally with the data router (one mock stream, partitioned to many grids), but depends on it no more than a real socket does.</p>
5273
5333
  <pre><code>import { MockWebSocket, opsFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
@@ -6287,6 +6347,38 @@ const rows = stream.map((temp, t) =&gt; ({ t, temp }));
6287
6347
  const signal = condition(rows);
6288
6348
  return `methods ${ROLLING_ANOMALY_METHODS.length}; spike ${roll.flags[10]}; alert ${signal ? signal.flagged.length : 0}`;</code></pre>
6289
6349
 
6350
+ <h3 id="forecasting">Forecasting</h3>
6351
+ <p class="section-note">Project an ordered series forward, and carry a prediction band where a defensible closed form exists (BACKLOG-0000963). Five methods behind one entry point: <code>movingAverage</code> and <code>ses</code> are flat forecasts (the trailing-window mean, the final smoothed level); <code>holt</code> adds a projected trend, <code>holtWinters</code> a projected trend and an additive seasonal; <code>linear</code> extrapolates an ordinary least-squares fit of the time axis. The exponential-smoothing bands are the innovations state-space forecast variances (Hyndman &amp; Athanasopoulos) at the normal quantile; the linear and moving-average bands are the exact Student-t intervals, and <code>linear</code> also reports the narrower mean-response (confidence) band a trendline draws. These are the pure kernels the chart trendline overlay and the time-series grid forecast from; a host can forecast a plain array the same way.</p>
6352
+ <div class="table-wrap">
6353
+ <table>
6354
+ <thead><tr><th>Name</th><th>Signature</th><th>Description</th></tr></thead>
6355
+ <tbody>
6356
+ <tr><td class="name">FORECAST_METHODS</td><td class="type">readonly ('movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear')[]</td><td class="desc">The five methods a caller may ask for, named so a result can say which produced it.</td></tr>
6357
+ <tr><td class="name">forecast</td><td class="type">(seq, opts?) =&gt; ForecastResult | null</td><td class="desc">Forecast an ordered series <code>opts.horizon</code> steps ahead by <code>opts.method</code> (default <code>linear</code>), at <code>opts.confidence</code> (default 0.95). Accepts a plain array of numbers (index is the time axis) or <code>{at, value}</code> rows. A smoothing factor absent from <code>opts</code> (<code>alpha</code>/<code>beta</code>/<code>gamma</code>) is fit by minimising the in-sample one-step SSE; <code>holtWinters</code> needs <code>opts.period</code> (≥ 2) and two whole periods of data. Each point carries <code>mean</code> and, where a band applies, <code>lower</code>/<code>upper</code>; <code>linear</code> adds <code>lowerMean</code>/<code>upperMean</code>. Null when the series is too short for the method.</td></tr>
6358
+ </tbody>
6359
+ </table>
6360
+ </div>
6361
+ <pre data-run="js" data-expect="methods 5; next 5.8; r2 0.6; band 1.68..9.92; season 63.125" data-covers="export:FORECAST_METHODS export:forecast"><code>const { forecast, FORECAST_METHODS } = await import('../packages/core/src/index.js');
6362
+ // Linear: fit the time axis, project one step, and carry the prediction band.
6363
+ const lin = forecast([{ at: 1, value: 2 }, { at: 2, value: 4 }, { at: 3, value: 5 }, { at: 4, value: 4 }, { at: 5, value: 5 }], { method: 'linear', horizon: 1 });
6364
+ const p = lin.points[0];
6365
+ // Holt-Winters additive: a level, a trend and a two-step season, projected two
6366
+ // steps ahead — the seasonal swing is carried into the forecast, not smoothed away.
6367
+ const hw = forecast([10, 20, 30, 40], { method: 'holtWinters', period: 2, alpha: 0.5, beta: 0.5, gamma: 0.5, horizon: 2 });
6368
+ return `methods ${FORECAST_METHODS.length}; next ${p.mean.toFixed(1)}; r2 ${lin.r2.toFixed(1)}; band ${p.lower.toFixed(2)}..${p.upper.toFixed(2)}; season ${hw.points[1].mean}`;</code></pre>
6369
+
6370
+ <p class="section-note">The same forecast off the standard stats surface: <code>grid.statistics.forecast(colId, opts)</code> reads the column over the filtered rows — ordered by <code>opts.by</code> when the time axis matters, exactly as <code>grid.statistics.series(...)</code> orders — and returns the same <code>ForecastResult</code>, so a host reaches a forecast the way it reaches <code>grid.statistics.anomalies(...)</code> rather than assembling the series itself.</p>
6371
+ <pre data-run="js" data-expect="next 5.8; r2 0.6; band true" data-covers="method:statistics"><code>const { createHeadlessGrid } = await import('../packages/core/src/index.js');
6372
+ const grid = createHeadlessGrid({
6373
+ columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }],
6374
+ rows: [[1, 2], [2, 4], [3, 5], [4, 4], [5, 5]].map(([t, v], i) =&gt; ({ id: String(i), t, v })),
6375
+ rowKey: 'id',
6376
+ });
6377
+ // Forecast the 'v' column one step ahead, ordered by 't', with the prediction band.
6378
+ const f = grid.statistics.forecast('v', { by: 't', method: 'linear', horizon: 1 });
6379
+ const p = f.points[0];
6380
+ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.lower &gt; 0}`;</code></pre>
6381
+
6290
6382
 
6291
6383
  <!-- BEGIN GENERATED TYPE REFERENCE -->
6292
6384
  <h2 id="type-reference">Type reference</h2>
@@ -7828,6 +7920,40 @@ return `methods ${ROLLING_ANOMALY_METHODS.length}; spike ${roll.flags[10]}; aler
7828
7920
  </tbody>
7829
7921
  </table>
7830
7922
  </div>
7923
+ <h3 id="type-ForecastPoint">ForecastPoint</h3>
7924
+ <p class="section-note">One forecast step: the point estimate and, where a band applies, its interval.</p>
7925
+ <div class="table-wrap">
7926
+ <table>
7927
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7928
+ <tbody>
7929
+ <tr><td class="name">step</td><td class="type">number</td><td class="desc">The step ahead, `1 … horizon`.</td></tr>
7930
+ <tr><td class="name">at</td><td class="type">number</td><td class="desc">The time-axis position the step is stamped at, extrapolated at the mean spacing.</td></tr>
7931
+ <tr><td class="name">mean</td><td class="type">number</td><td class="desc">The point forecast.</td></tr>
7932
+ <tr><td class="name">lower</td><td class="type">number | null</td><td class="desc">The prediction-interval lower bound (a future observation), or null when none applies.</td></tr>
7933
+ <tr><td class="name">upper</td><td class="type">number | null</td><td class="desc">The prediction-interval upper bound, or null when none applies.</td></tr>
7934
+ <tr><td class="name">lowerMean</td><td class="type">number | null</td><td class="desc">The mean-response (confidence) lower bound — `linear` only, the band a trendline draws. <small>(optional)</small></td></tr>
7935
+ <tr><td class="name">upperMean</td><td class="type">number | null</td><td class="desc">The mean-response (confidence) upper bound — `linear` only. <small>(optional)</small></td></tr>
7936
+ <tr><td class="name">se</td><td class="type">number | null</td><td class="desc">The prediction standard error the band was built from, or null when none applies.</td></tr>
7937
+ </tbody>
7938
+ </table>
7939
+ </div>
7940
+ <h3 id="type-ForecastResult">ForecastResult</h3>
7941
+ <p class="section-note">A forecast: the chosen model, its parameters, and the projected points.</p>
7942
+ <div class="table-wrap">
7943
+ <table>
7944
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7945
+ <tbody>
7946
+ <tr><td class="name">method</td><td class="type">'movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear'</td><td class="desc">Which method produced it.</td></tr>
7947
+ <tr><td class="name">horizon</td><td class="type">number</td><td class="desc">How many steps ahead were projected.</td></tr>
7948
+ <tr><td class="name">confidence</td><td class="type">number</td><td class="desc">The band level, e.g. 0.95.</td></tr>
7949
+ <tr><td class="name">n</td><td class="type">number</td><td class="desc">How many finite readings the fit used.</td></tr>
7950
+ <tr><td class="name">sigma</td><td class="type">number | null</td><td class="desc">The residual standard deviation the bands were built from, or null when there was none.</td></tr>
7951
+ <tr><td class="name">r2</td><td class="type">number</td><td class="desc">The fit's coefficient of determination — `linear` only. <small>(optional)</small></td></tr>
7952
+ <tr><td class="name">params</td><td class="type">{</td><td class="desc">The model parameters: `slope`/`intercept` (linear), `alpha`/`beta`/`gamma`/`period`, or `windowLen`.</td></tr>
7953
+ <tr><td class="name">points</td><td class="type">ForecastPoint[]</td><td class="desc">The forecast, one entry per step.</td></tr>
7954
+ </tbody>
7955
+ </table>
7956
+ </div>
7831
7957
  <h3 id="type-FormattingApi">FormattingApi</h3>
7832
7958
  <div class="table-wrap">
7833
7959
  <table>
@@ -8268,6 +8394,8 @@ return `methods ${ROLLING_ANOMALY_METHODS.length}; spike ${roll.flags[10]}; aler
8268
8394
  <tbody>
8269
8395
  <tr><td class="name">preview</td><td class="type">(text: string, opts?: object): ImportPreview</td><td class="desc">Parse delimited text into a preview, changing nothing.</td></tr>
8270
8396
  <tr><td class="name">csv</td><td class="type">(text: string, opts?: object): Record&lt;string, unknown&gt;[]</td><td class="desc">Parse delimited text into coerced records — the inverse of `export.csv`.</td></tr>
8397
+ <tr><td class="name">previewXlsx</td><td class="type">(bytes: Uint8Array | ArrayBuffer, opts?: object): Promise&lt;ImportXlsxPreview&gt;</td><td class="desc">Parse an `.xlsx` file's bytes into a preview, changing nothing (§14, BACKLOG-0000970). Async: the archive is inflated with `DecompressionStream`.</td></tr>
8398
+ <tr><td class="name">xlsx</td><td class="type">(bytes: Uint8Array | ArrayBuffer, opts?: object): Promise&lt;Record&lt;string, unknown&gt;[]&gt;</td><td class="desc">Parse an `.xlsx` file's bytes into coerced records — the inverse of `export.excel`.</td></tr>
8271
8399
  <tr><td class="name">apply</td><td class="type">(</td><td class="desc">Add or replace the grid's rows from text, a preview or records.</td></tr>
8272
8400
  </tbody>
8273
8401
  </table>
@@ -8316,6 +8444,22 @@ return `methods ${ROLLING_ANOMALY_METHODS.length}; spike ${roll.flags[10]}; aler
8316
8444
  </tbody>
8317
8445
  </table>
8318
8446
  </div>
8447
+ <h3 id="type-ImportXlsxPreview">ImportXlsxPreview</h3>
8448
+ <p class="section-note">What an `.xlsx` preview carries — an {@link ImportPreview} plus the sheet read (§14).</p>
8449
+ <div class="table-wrap">
8450
+ <table>
8451
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8452
+ <tbody>
8453
+ <tr><td class="name">sheet</td><td class="type">string | null</td><td class="desc">The archive path of the worksheet that was read, e.g. `xl/worksheets/sheet1.xml`.</td></tr>
8454
+ <tr><td class="name">header</td><td class="type">string[]</td><td class="desc">The source column headings.</td></tr>
8455
+ <tr><td class="name">columns</td><td class="type">ImportColumn[]</td><td class="desc">The per-column mapping and inference the user may edit before confirming.</td></tr>
8456
+ <tr><td class="name">records</td><td class="type">Record&lt;string, unknown&gt;[]</td><td class="desc">Every mapped, coerced record the import would add.</td></tr>
8457
+ <tr><td class="name">sample</td><td class="type">Record&lt;string, unknown&gt;[]</td><td class="desc">The leading records, for a preview table.</td></tr>
8458
+ <tr><td class="name">rowCount</td><td class="type">number</td><td class="desc">How many data rows the sheet holds.</td></tr>
8459
+ <tr><td class="name">warnings</td><td class="type">string[]</td><td class="desc">Anything worth flagging before confirming.</td></tr>
8460
+ </tbody>
8461
+ </table>
8462
+ </div>
8319
8463
  <h3 id="type-IngestConfig">IngestConfig</h3>
8320
8464
  <p class="section-note">How rows are ingested into the column store.</p>
8321
8465
  <div class="table-wrap">
@@ -9431,6 +9575,7 @@ return `methods ${ROLLING_ANOMALY_METHODS.length}; spike ${roll.flags[10]}; aler
9431
9575
  <tr><td class="name">capability</td><td class="type">(colId: string, opts?: {</td><td class="desc">Process capability against the column's `spec`, with control limits and the Western Electric rule breaks. `baseline` fixes the limits over the first N readings, which is how a shift is found rather than hidden by the limits it widened.</td></tr>
9432
9576
  <tr><td class="name">interval</td><td class="type">(colId: string, opts?: {</td><td class="desc">A confidence interval for what a column measures, the range the estimate pins the figure down to, not a verdict about it. Reads the rows the filters left, so an interval narrows as the grid does: it describes the filtered population, not the whole table.</td></tr>
9433
9577
  <tr><td class="name">series</td><td class="type">(colId: string, opts: { by: string; periodsPerYear?: number }): SeriesStats | null</td><td class="desc">How a column varies along an ordering. `by` is required and never guessed: kernels see rows in the order they arrived, which is not the grid's sort.</td></tr>
9578
+ <tr><td class="name">forecast</td><td class="type">(colId: string, opts?: {</td><td class="desc">Forecast one column forward (BACKLOG-0000963): the stats-surface face of the {@link forecast} kernel. The column is read over the filtered rows in arrival order, or ordered by `opts.by` (a date or numeric column, as {@link series} orders) when the time axis matters, then projected `opts.horizon` steps ahead by `opts.method` (default `linear`) with a prediction band where one applies. Every kernel option passes through; returns the same {@link ForecastResult}, or null when the column is unknown or too short.</td></tr>
9434
9579
  <tr><td class="name">weightedAverage</td><td class="type">(colId: string, weightId: string): number | null</td><td class="desc">A weighted average of one column by another.</td></tr>
9435
9580
  <tr><td class="name">keyOf</td><td class="type">(data: unknown): string | null</td><td class="desc">The key a row's data resolves to.</td></tr>
9436
9581
  <tr><td class="name">maintenance</td><td class="type">Readonly&lt;Record&lt;string, 'maintained' | 'rescan'&gt;&gt;</td><td class="desc">Which reductions can be maintained against a change, and which rescan. <small>(read-only)</small></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.39.0</p>
440
+ <p class="rail__sub">Developer guide · v1.42.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -6343,6 +6343,7 @@ grid.import.apply(preview);</code></pre>
6343
6343
  <tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. By default routing is first-match-wins (<code>overlap: false</code>); to fan one partition value to several viewers at once (a grid <em>and</em> a KPI panel <em>and</em> a chart off one feed) create the router with <code>overlap: true</code> — with the default, a second viewer on the same value receives nothing and the router emits a one-time dev warning naming the clash. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. v3 also adds per-route reshaping — <code>transform</code>/<code>filter</code>/<code>sort</code> and <code>rollup</code> ({ groupBy, aggregate }) summaries — a relationship graph (<code>relate(edges)</code>: multi-hop, several-into-one AND, and mutual edges) that scales v2's pairwise <code>link</code>, and stream hygiene: a <code>seq</code>/version orders and de-duplicates a feed (stale/duplicate deltas dropped, counted in <code>dropped</code>), <code>push</code> with a <code>batch</code>/<code>coalesce</code> buffers a high-frequency feed (<code>flushStream</code> for a deterministic point), and <code>lastSeq</code>/<code>checkpoint</code>/<code>seenThrough</code> resume precisely after a dropped socket. v4 adds time-travel: <code>buffer({ window, max })</code> records the ordered stream into a bounded ring over a moving base, so <code>scrubTo</code> reconstructs a past point, <code>replay</code> (with <code>pause</code>/<code>resume</code>) walks a range, and <code>live</code> returns to the head — every state pushed by the same keyed diff, <code>traveling</code>/<code>buffered</code> reporting the state. v6 adds cross-tab sync: <code>broadcast({ channel })</code> mirrors the ordered, de-duplicated deltas to other tabs/windows over a BroadcastChannel with no echo loop, a popped-out grid joining the same feed with no second socket and resyncing mid-stream via the reconnect path. v7 adds query-slice routing: <code>query(adapter, request)</code> sources the router from a DFQL/DuckDB (or any pushdown) adapter, partitioning one result across the routes; a route-level <code>where</code> is pushed down where the adapter's capabilities allow and the residual finished client-side, with <code>lastQueryPlan</code> reporting the split. v8 adds write-back: a <code>writable</code> route captures the grid's committed edits off its public edit surface and routes them to <code>onWrite(change, ctx)</code> (per-route or router-global), reverting on reject, re-entering an accepted write as a delta, and surfacing a last-write-wins <code>onConflict</code>; a derived route cannot be writable. v9 adds fan-in: <code>addSource(feed, { map, key })</code> returns a per-feed handle (<code>load</code>/<code>apply</code>/<code>push</code>/<code>remove</code>) whose rows are normalized and namespaced so many feeds merge into one keyed store without id collisions, <code>removeSource</code> dropping exactly a feed's rows and <code>sources</code> listing them. v10 adds observability: <code>metrics()</code> is a cheap snapshot of per-route/per-source counts and throughput plus the global unrouted/dropped/buffered/lag figures, <code>on('metrics')</code> drives a periodic emit (off unless a listener is registered), and <code>mountDevtools(el)</code> renders a live panel from the module's own DOM file. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
6344
6344
  <tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane&times;column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
6345
6345
  <tr><td class="name">createKPI</td><td class="desc">A KPI / stat-tile view (module <code>kpi</code>) of a dataset as a panel of aggregate tiles — each tile a <code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code> or a <code>custom</code> reducer over the routed rows, with an optional <code>filter</code> predicate, number <code>format</code> (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>), a <code>baseline</code> for a delta, semantic threshold bands (<code>thresholds</code> with two cut points and a direction, or an explicit <code>bands</code> list, giving a <code>good</code>/<code>warn</code>/<code>critical</code> status kept separate from any accent), and an optional <code>sparkline</code> series. It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, kpi)</code> and drive a KPI panel beside a grid, a kanban and a chart off one feed. Updates are incremental — a delta adjusts each tile's running accumulator by only the rows it carries (an add contributes, a remove reverses, an update reverses-then-contributes), the two bounded exceptions being a <code>min</code>/<code>max</code> whose current extreme is removed (a rescan of that tile's own value multiset) and a <code>custom</code> reducer (recomputed over the filtered store, an arbitrary function having no inverse). Each tile is a labelled <code>&lt;figure&gt;</code>, focusable and keyboard-activatable, its value announced, the sparkline respecting <code>prefers-reduced-motion</code>; a <code>tile:click</code> event (also from the keyboard) lets a host drill down or filter a routed grid. Pass <code>null</code> as the element for a headless panel that computes the same tile model without a DOM. Not a dashboard layout engine (that is the parked dashboard generator) and no charting beyond the minimal sparkline (that is the charts module).</td></tr>
6346
+ <tr><td class="name">createAI</td><td class="desc">Create an AI narrative / insights controller (module <code>ai</code>) over a live grid. It produces a short, plain-language narrative of the grid's <em>computed</em> figures — a per-KPI / per-chart / per-column <code>explain</code>, or an <code>insights</code> panel over the current filtered view. The grid makes no AI call of its own: <code>createAI</code> imports no provider SDK, holds no key, and makes no network request; it calls one host callback, <code>ask({ system, messages, prompt, tools?, schema?, signal })</code> — your model, your key, your privacy decision — the same philosophy as the data adapters. Two grounding paths feed one guard: where the provider offers tool-use the model is given a curated read-only tool set (<code>getSchema</code>, <code>getProfile</code>, <code>getStatistics</code>, <code>getForecast</code>, <code>runQuery</code>) and the grid's own engine computes what it asks for; otherwise the module builds a facts packet from <code>grid.statistics</code>/the profile/forecasts/view counts and passes it in the prompt. Every figure in the narrative is reconciled against the values the engine produced this render — an ungrounded number is stripped before display (the number-reconciliation guard), so a hallucinated figure never reaches the user. The prompt is constrained to narrate-only; the layer is read-only and never mutates data. <code>redact</code> (a column id, a list, or a predicate) and <code>maxRows</code> bound what the module hands <code>ask()</code>, and the module sends nothing itself; an <code>ask()</code> error surfaces a friendly message and the grid stays fully usable, AI being additive rather than load-bearing. Complementary to <code>grid.ai</code> (the intent/plan skill layer): pass no <code>ask</code> and it adopts the grid's configured <code>ai.ask</code>, running the facts-packet path over it. Pass a headless grid for a DOM-free narrative; <code>facts(target)</code> returns the exact grounded packet without calling <code>ask()</code>. UMD global <code>LatticeGridAI</code>.</td></tr>
6346
6347
  <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
6347
6348
  <tr><td class="name">createGantt</td><td class="desc">Create a project-planning Gantt controller (module <code>gantt</code>) over a task list and a dependency list. A CPM engine (<code>computeSchedule</code>) computes each task's early/late start and finish, its slack and the zero-float critical path, honouring the four link types (<code>LINK_TYPES</code>: FS/SS/FF/SF) with lag, and recomputes on every edit — emitting <code>schedule</code> or, on a dependency cycle or bad input, <code>error</code> (a code from <code>SCHEDULE_ERROR</code>). Milestones are zero-duration points; summary (WBS) tasks are derived from their children (earliest start, latest finish, weighted progress) rather than scheduled; <code>findViolations</code> flags a task placed earlier than its predecessors allow, and <code>toISODate</code> maps an engine day-number back to a calendar date.</td></tr>
6348
6349
  <tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.39.0, type declarations
2
+ * Lattice Grid 1.42.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -2774,6 +2774,22 @@ export interface StatisticsApi {
2774
2774
  * kernels see rows in the order they arrived, which is not the grid's sort.
2775
2775
  */
2776
2776
  series(colId: string, opts: { by: string; periodsPerYear?: number }): SeriesStats | null;
2777
+ /**
2778
+ * Forecast one column forward (BACKLOG-0000963): the stats-surface face of the
2779
+ * {@link forecast} kernel. The column is read over the filtered rows in arrival
2780
+ * order, or ordered by `opts.by` (a date or numeric column, as {@link series}
2781
+ * orders) when the time axis matters, then projected `opts.horizon` steps ahead
2782
+ * by `opts.method` (default `linear`) with a prediction band where one applies.
2783
+ * Every kernel option passes through; returns the same {@link ForecastResult},
2784
+ * or null when the column is unknown or too short.
2785
+ */
2786
+ forecast(colId: string, opts?: {
2787
+ method?: 'movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear';
2788
+ horizon?: number; confidence?: number; windowLen?: number;
2789
+ alpha?: number; beta?: number; gamma?: number; period?: number;
2790
+ /** The column to order by before forecasting — a date or numeric axis. */
2791
+ by?: string;
2792
+ }): ForecastResult | null;
2777
2793
  /** A weighted average of one column by another. */
2778
2794
  weightedAverage(colId: string, weightId: string): number | null;
2779
2795
  /** The key a row's data resolves to. */
@@ -2989,6 +3005,80 @@ export function anomalyCondition(
2989
3005
  ): (rows: Iterable<Record<string, unknown>>) =>
2990
3006
  false | { method: string; field: string; flagged: { row: Record<string, unknown>; score: number | null }[] };
2991
3007
 
3008
+ /**
3009
+ * The forecasting methods a caller may ask for (BACKLOG-0000963), named so a
3010
+ * result says which produced it: a trailing moving average, single / double
3011
+ * (Holt) / triple (Holt-Winters) exponential smoothing, and a linear least-squares
3012
+ * fit of the time axis.
3013
+ */
3014
+ export const FORECAST_METHODS: readonly ('movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear')[];
3015
+
3016
+ /** One forecast step: the point estimate and, where a band applies, its interval. */
3017
+ export interface ForecastPoint {
3018
+ /** The step ahead, `1 … horizon`. */
3019
+ step: number;
3020
+ /** The time-axis position the step is stamped at, extrapolated at the mean spacing. */
3021
+ at: number;
3022
+ /** The point forecast. */
3023
+ mean: number;
3024
+ /** The prediction-interval lower bound (a future observation), or null when none applies. */
3025
+ lower: number | null;
3026
+ /** The prediction-interval upper bound, or null when none applies. */
3027
+ upper: number | null;
3028
+ /** The mean-response (confidence) lower bound — `linear` only, the band a trendline draws. */
3029
+ lowerMean?: number | null;
3030
+ /** The mean-response (confidence) upper bound — `linear` only. */
3031
+ upperMean?: number | null;
3032
+ /** The prediction standard error the band was built from, or null when none applies. */
3033
+ se: number | null;
3034
+ }
3035
+
3036
+ /** A forecast: the chosen model, its parameters, and the projected points. */
3037
+ export interface ForecastResult {
3038
+ /** Which method produced it. */
3039
+ method: 'movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear';
3040
+ /** How many steps ahead were projected. */
3041
+ horizon: number;
3042
+ /** The band level, e.g. 0.95. */
3043
+ confidence: number;
3044
+ /** How many finite readings the fit used. */
3045
+ n: number;
3046
+ /** The residual standard deviation the bands were built from, or null when there was none. */
3047
+ sigma: number | null;
3048
+ /** The fit's coefficient of determination — `linear` only. */
3049
+ r2?: number;
3050
+ /** The model parameters: `slope`/`intercept` (linear), `alpha`/`beta`/`gamma`/`period`, or `windowLen`. */
3051
+ params: {
3052
+ slope?: number; intercept?: number;
3053
+ alpha?: number; beta?: number; gamma?: number; period?: number; windowLen?: number;
3054
+ };
3055
+ /** The forecast, one entry per step. */
3056
+ points: ForecastPoint[];
3057
+ }
3058
+
3059
+ /**
3060
+ * Forecast an ordered series `horizon` steps into the future (BACKLOG-0000963).
3061
+ *
3062
+ * `movingAverage` and `ses` are flat forecasts (the trailing-window mean, the
3063
+ * final smoothed level); `holt` adds a projected trend, `holtWinters` a projected
3064
+ * trend and an additive seasonal of period `opts.period`; `linear` extrapolates
3065
+ * an ordinary least-squares fit of the time axis. A prediction band is carried
3066
+ * where a defensible closed form exists — the exponential-smoothing bands are the
3067
+ * innovations state-space forecast variances at the normal quantile; the linear
3068
+ * and moving-average bands are the exact Student-t intervals, and `linear` also
3069
+ * reports the narrower mean-response (confidence) band. A smoothing factor absent
3070
+ * from `opts` is fit by minimising the in-sample one-step SSE. Returns null when
3071
+ * the series is too short for the chosen method.
3072
+ */
3073
+ export function forecast(
3074
+ seq: ArrayLike<number | null> | { at?: number; value: number | null }[],
3075
+ opts?: {
3076
+ method?: 'movingAverage' | 'ses' | 'holt' | 'holtWinters' | 'linear';
3077
+ horizon?: number; confidence?: number; windowLen?: number;
3078
+ alpha?: number; beta?: number; gamma?: number; period?: number;
3079
+ },
3080
+ ): ForecastResult | null;
3081
+
2992
3082
  export type ShadowKind =
2993
3083
  | 'updates' | 'updatedAt' | 'sinceUpdate' | 'delta' | 'deltaPercent'
2994
3084
  | 'rate' | 'history' | 'firstValue' | 'streak'
@@ -4186,12 +4276,37 @@ export interface ImportPreview {
4186
4276
  warnings: string[];
4187
4277
  }
4188
4278
 
4279
+ /** What an `.xlsx` preview carries — an {@link ImportPreview} plus the sheet read (§14). */
4280
+ export interface ImportXlsxPreview {
4281
+ /** The archive path of the worksheet that was read, e.g. `xl/worksheets/sheet1.xml`. */
4282
+ sheet: string | null;
4283
+ /** The source column headings. */
4284
+ header: string[];
4285
+ /** The per-column mapping and inference the user may edit before confirming. */
4286
+ columns: ImportColumn[];
4287
+ /** Every mapped, coerced record the import would add. */
4288
+ records: Record<string, unknown>[];
4289
+ /** The leading records, for a preview table. */
4290
+ sample: Record<string, unknown>[];
4291
+ /** How many data rows the sheet holds. */
4292
+ rowCount: number;
4293
+ /** Anything worth flagging before confirming. */
4294
+ warnings: string[];
4295
+ }
4296
+
4189
4297
  /** Bringing rows in — the mirror of {@link ExportApi} (§14, BACKLOG-0000949). */
4190
4298
  export interface ImportApi {
4191
4299
  /** Parse delimited text into a preview, changing nothing. */
4192
4300
  preview(text: string, opts?: object): ImportPreview;
4193
4301
  /** Parse delimited text into coerced records — the inverse of `export.csv`. */
4194
4302
  csv(text: string, opts?: object): Record<string, unknown>[];
4303
+ /**
4304
+ * Parse an `.xlsx` file's bytes into a preview, changing nothing (§14,
4305
+ * BACKLOG-0000970). Async: the archive is inflated with `DecompressionStream`.
4306
+ */
4307
+ previewXlsx(bytes: Uint8Array | ArrayBuffer, opts?: object): Promise<ImportXlsxPreview>;
4308
+ /** Parse an `.xlsx` file's bytes into coerced records — the inverse of `export.excel`. */
4309
+ xlsx(bytes: Uint8Array | ArrayBuffer, opts?: object): Promise<Record<string, unknown>[]>;
4195
4310
  /** Add or replace the grid's rows from text, a preview or records. */
4196
4311
  apply(
4197
4312
  input: string | ImportPreview | Record<string, unknown>[],
@@ -7885,3 +8000,142 @@ declare module 'lattice-grid/modules/kpi' {
7885
8000
  export function createKPI(el: HTMLElement | null, config?: KPIConfig): KPI;
7886
8001
  export default createKPI;
7887
8002
  }
8003
+
8004
+ declare module 'lattice-grid/modules/ai' {
8005
+ /**
8006
+ * The provider-agnostic model callback the host supplies (BACKLOG-0000965).
8007
+ * The module never imports a provider SDK, reads a key, or makes a network
8008
+ * call — it builds this payload and awaits the host's reply. A host may wrap a
8009
+ * chat provider (`{ text }`), a completion (a bare string), a tool-calling turn
8010
+ * (`{ toolCalls }`), or a structured provider (`{ structured }`).
8011
+ */
8012
+ type AIAsk = (payload: {
8013
+ /** The narrate-only system instruction. */
8014
+ system: string;
8015
+ /** The single user message: the facts block and the ask. */
8016
+ message: string;
8017
+ /** System and message joined, for a completion-shaped provider. */
8018
+ prompt: string;
8019
+ /** The running chat, including any tool results, for a chat-shaped provider. */
8020
+ messages: Array<{ role: string; content: string; [k: string]: unknown }>;
8021
+ /** The read-only tool definitions, present only on the tool-use path. */
8022
+ tools?: object[];
8023
+ /** The grid's generated schema (no row values). */
8024
+ schema?: unknown;
8025
+ /** An abort signal the host should honour. */
8026
+ signal?: AbortSignal;
8027
+ }) => Promise<
8028
+ | string
8029
+ | { text?: string; content?: string; toolCalls?: object[]; structured?: unknown }
8030
+ >;
8031
+
8032
+ /** A single computed figure a narrative is grounded on. */
8033
+ interface AIFact {
8034
+ id: string;
8035
+ label: string;
8036
+ /** The raw numeric value, or null for a context-only fact. */
8037
+ value: number | null;
8038
+ /** The pre-formatted display string the model is told to use verbatim. */
8039
+ display: string;
8040
+ kind: string;
8041
+ colId?: string;
8042
+ }
8043
+
8044
+ /**
8045
+ * A narrative target. `view` narrates the current filtered view; `column`
8046
+ * narrates one column's profile; `forecast` adds its projection; `kpi`/`chart`
8047
+ * narrate figures the caller passes through in `facts`.
8048
+ */
8049
+ interface AITarget {
8050
+ kind?: 'view' | 'column' | 'forecast' | 'kpi' | 'chart';
8051
+ colId?: string;
8052
+ /** Forecast options, for `kind: 'forecast'`. */
8053
+ options?: object;
8054
+ /** Caller-supplied figures for a KPI/chart Explain, grounded like the rest. */
8055
+ facts?: Array<{ id?: string; label: string; value: unknown; display?: string; kind?: string; colId?: string }>;
8056
+ }
8057
+
8058
+ /** The facts packet a narrative grounds on. */
8059
+ interface AIFactsPacket {
8060
+ target: AITarget;
8061
+ facts: AIFact[];
8062
+ /** The numeric values seeding the reconciliation registry. */
8063
+ groundedValues: number[];
8064
+ meta: { kind: string; filtered: boolean; factCount: number; redacted?: boolean; colId?: string };
8065
+ }
8066
+
8067
+ /** The result of a narrative: reconciled prose plus what grounded and what did not. */
8068
+ interface AINarrative {
8069
+ /** The narrative, with every ungrounded figure stripped (or flagged). */
8070
+ text: string;
8071
+ facts: AIFact[];
8072
+ /** The figures that reconciled against a computed value. */
8073
+ grounded: string[];
8074
+ /** The figures removed as ungrounded. */
8075
+ flagged: string[];
8076
+ packet: AIFactsPacket;
8077
+ /** How many ask() rounds ran (>1 only on the tool-use path). */
8078
+ rounds: number;
8079
+ mode: 'tools' | 'packet';
8080
+ }
8081
+
8082
+ /** AI module configuration. */
8083
+ interface AIConfig {
8084
+ /** The host's model callback. Falls back to the grid's `ai.ask` when omitted. */
8085
+ ask?: AIAsk;
8086
+ /** Opt into specific features: `'narrative'`, `'insights'`. All on when omitted. */
8087
+ enable?: string[];
8088
+ /** Cap on rows any tool result carries to `ask()`. */
8089
+ maxRows?: number;
8090
+ /** Columns whose values must never leave the browser. */
8091
+ redact?: string | string[] | ((colId: string) => boolean);
8092
+ /** Force tool-use on or off; auto-detected from how `ask` was supplied otherwise. */
8093
+ tools?: boolean;
8094
+ /** Locale for figure formatting. */
8095
+ locale?: string;
8096
+ /** Column cap for a view summary. */
8097
+ maxColumns?: number;
8098
+ /** What to do with an ungrounded figure: `'strip'` (default) or `'flag'`. */
8099
+ reconcile?: 'strip' | 'flag';
8100
+ /** An element to mount the insights panel into. */
8101
+ element?: HTMLElement;
8102
+ /** Called when a narrative is produced. */
8103
+ onNarrative?: (result: AINarrative) => void;
8104
+ /** Called when `ask()` errors; the grid stays usable. */
8105
+ onError?: (error: { error: unknown; target: AITarget }) => void;
8106
+ }
8107
+
8108
+ /**
8109
+ * An AI narrative / insights controller over a live grid. Read-only: it
8110
+ * explains the grid's computed figures and never mutates data. `grid.ai` (in
8111
+ * core) is the complementary intent/plan skill layer; this is the
8112
+ * narrative/insights consumer.
8113
+ */
8114
+ interface AI {
8115
+ /** The mounted insights panel element, or null. */
8116
+ readonly el: HTMLElement | null;
8117
+ /** Whether a usable `ask()` is configured. */
8118
+ readonly ready: boolean;
8119
+ /** Produce a grounded, reconciled narrative for a target. */
8120
+ explain(target?: AITarget, opts?: object): Promise<AINarrative>;
8121
+ /** An alias for {@link AI.explain}. */
8122
+ narrate(target?: AITarget, opts?: object): Promise<AINarrative>;
8123
+ /** Mount (or re-target) the insights panel into an element. */
8124
+ insights(el?: HTMLElement, opts?: object): AI;
8125
+ /** Build an "Explain" button bound to a target. */
8126
+ attachExplain(target: AITarget, opts?: object): HTMLElement | null;
8127
+ /** Build the facts packet for a target without calling `ask()`. */
8128
+ facts(target?: AITarget, opts?: object): AIFactsPacket;
8129
+ on(name: 'narrative' | 'error' | string, fn: (payload: object) => void): () => void;
8130
+ off(name: string, fn: (payload: object) => void): void;
8131
+ destroy(): void;
8132
+ }
8133
+
8134
+ /**
8135
+ * Create an AI narrative / insights controller over a live grid. The grid may
8136
+ * be headless or rendered; the module grounds every figure on the grid's
8137
+ * engine and calls only the host's `ask()`.
8138
+ */
8139
+ export function createAI(grid: unknown, config?: AIConfig): AI;
8140
+ export default createAI;
8141
+ }