@toclocoinc/lattice-grid 1.40.0 → 1.43.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 +95 -0
  3. package/docs/api-detail.html +2 -1
  4. package/lattice-grid.d.ts +212 -1
  5. package/lattice-grid.esm.min.js +4 -4
  6. package/lattice-grid.min.cjs +4 -4
  7. package/lattice-grid.min.js +4 -4
  8. package/modules/ai.esm.min.js +3369 -0
  9. package/modules/ai.min.cjs +3372 -0
  10. package/modules/ai.min.js +3372 -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 +4 -4
  36. package/modules/data-router.min.cjs +4 -4
  37. package/modules/data-router.min.js +4 -4
  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 +4 -4
  48. package/modules/htmx.min.cjs +4 -4
  49. package/modules/htmx.min.js +4 -4
  50. package/modules/kanban.esm.min.js +4 -4
  51. package/modules/kanban.min.cjs +4 -4
  52. package/modules/kanban.min.js +4 -4
  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 +4 -4
  69. package/modules/webcomponent.min.cjs +4 -4
  70. package/modules/webcomponent.min.js +4 -4
  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.40.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.43.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
package/docs/API.html CHANGED
@@ -2498,6 +2498,41 @@ grid.permissions.setContext({ role: 'clerk' }); // re-resolves everything</
2498
2498
  </div>
2499
2499
  <div class="note"><p>Actions: <code>setFilters</code>, <code>setSort</code>, <code>groupBy</code>, <code>showColumns</code>, <code>hideColumns</code>, <code>setQuick</code>, <code>clear</code>. Nothing else is executable, so a model cannot be talked into an operation the vocabulary does not contain. <code>docs/AI-SKILL.md</code> is the reference to hand your model.</p></div>
2500
2500
 
2501
+ <h3 id="ai-askdata">Ask-your-data (<code>modules/ai</code>)</h3>
2502
+ <p>The opt-in AI module (<code>createAI</code>, UMD <code>LatticeGridAI</code>) turns a question into
2503
+ a validated <strong>read-only</strong> query spec, runs it in the grid's own engine over the
2504
+ <code>grid.ai</code> skill layer, and — on apply — fans the answer to any router-attached viewers
2505
+ (a chart, a KPI tile) through the Data Router's <code>load()</code>. It uses the same BYO
2506
+ <code>ask()</code> seam as the rest of the module: the grid makes no model call and holds no key.
2507
+ Ask-your-data is read-only; a write/mutation the model asks for is <strong>refused and never
2508
+ executed</strong> (writes are a separate, human-gated feature).</p>
2509
+ <div class="table-wrap">
2510
+ <table>
2511
+ <thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
2512
+ <tbody>
2513
+ <tr><td class="sig">query(question, opts?)</td><td class="type">Promise&lt;result&gt;</td><td class="desc">Ask the model, validate the reply into a read-only spec. <code>result.describe()</code> shows the resolved query; nothing applies until <code>result.apply()</code> (or <code>autoApply</code>).</td></tr>
2514
+ <tr><td class="sig">applyQuery(result, opts?)</td><td class="type">object</td><td class="desc">Apply a reviewed result. Re-gated at the seam: an unsafe plan is refused. <code>opts.router</code> fans the answer to other viewers.</td></tr>
2515
+ <tr><td class="sig">askBar(el, opts?)</td><td class="type">controller</td><td class="desc">Mount the ask/review/apply bar with an "auto-apply safe reads" toggle (off by default).</td></tr>
2516
+ </tbody>
2517
+ </table>
2518
+ </div>
2519
+ <pre data-run="js" data-expect="2 rows; write refused" data-covers="export:createAI"><code>const { createHeadlessGrid } = await import('../packages/core/src/index.js');
2520
+ const { createAI } = await import('../packages/modules/ai/index.js');
2521
+ const grid = createHeadlessGrid({
2522
+ rowKey: 'id',
2523
+ columns: [{ field: 'id' }, { field: 'region' }, { field: 'amount', type: 'number' }],
2524
+ rows: [{ id: 1, region: 'EMEA', amount: 100 }, { id: 2, region: 'AMER', amount: 300 }, { id: 3, region: 'EMEA', amount: 200 }],
2525
+ });
2526
+ // Your model returns a schema-constrained SPEC, never rows. WE run it, read-only.
2527
+ const reads = createAI(grid, { ask: async () =&gt; ({ actions: [{ type: 'setFilters', filters: { col: 'region', op: 'eq', value: 'EMEA' } }] }) });
2528
+ const result = await reads.query('EMEA only');
2529
+ result.apply();
2530
+ const rows = grid.rows.data().length;
2531
+ // A write verb is refused by the read-only gate and never executed.
2532
+ const writes = createAI(grid, { ask: async () =&gt; ({ actions: [{ type: 'setCells', edits: [] }] }) });
2533
+ const write = await writes.query('change the data');
2534
+ return `${rows} rows; write ${write.ok ? 'allowed' : 'refused'}`;</code></pre>
2535
+
2501
2536
  <h2 id="overlay">grid.overlay</h2>
2502
2537
  <div class="table-wrap">
2503
2538
  <table>
@@ -5268,6 +5303,66 @@ router.apply([{ op: 'delete', row: { id: 'd2' } }]); <span class="c
5268
5303
  router.destroy();
5269
5304
  <span class="kw">return</span> [seeded, afterRemove, band].join(' | ');</code></pre>
5270
5305
 
5306
+ <h2 id="ai">The AI narrative / insights layer</h2>
5307
+ <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>
5308
+ <pre><code>import { createAI } from '@toclocoinc/lattice-grid/modules/ai';
5309
+
5310
+ const ai = createAI(grid, {
5311
+ ask: async ({ system, messages, tools, schema, signal }) =&gt; {
5312
+ const r = await myProvider.chat({ system, messages, tools, signal });
5313
+ return { text: r.text, toolCalls: r.toolCalls }; <span class="cmt">// or a bare string</span>
5314
+ },
5315
+ maxRows: 50, <span class="cmt">// cap what any tool result carries to ask()</span>
5316
+ redact: ['ssn', 'salary'], <span class="cmt">// columns whose values never leave the browser</span>
5317
+ });
5318
+
5319
+ ai.insights(document.querySelector('#insights')); <span class="cmt">// the panel</span>
5320
+ const { text, flagged } = await ai.explain({ kind: 'column', colId: 'amount' });</code></pre>
5321
+ <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>
5322
+ <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>
5323
+ <div class="table-wrap">
5324
+ <table>
5325
+ <thead><tr><th>Member</th><th>Description</th></tr></thead>
5326
+ <tbody>
5327
+ <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>
5328
+ <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>
5329
+ <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>
5330
+ <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>
5331
+ <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>
5332
+ <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>
5333
+ </tbody>
5334
+ </table>
5335
+ </div>
5336
+ <h3 id="ai-reconcile-example">Grounded, with the reconciliation guard, executed</h3>
5337
+ <p class="section-note">A mock <code>ask()</code> returns one grounded figure (the row count) and one invented one.
5338
+ The number-reconciliation guard strips the ungrounded figure and keeps the grounded one. Run headless on every build.</p>
5339
+ <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');
5340
+ <span class="kw">const</span> { createAI } = <span class="kw">await</span> import('../packages/modules/ai/index.js');
5341
+
5342
+ <span class="kw">const</span> grid = createHeadlessGrid({
5343
+ rowKey: 'id',
5344
+ columns: [{ field: 'id' }, { field: 'region' }, { field: 'amount', type: 'number' }],
5345
+ rows: [
5346
+ { id: 1, region: 'EMEA', amount: 100 },
5347
+ { id: 2, region: 'AMER', amount: 300 },
5348
+ { id: 3, region: 'APAC', amount: 200 },
5349
+ ],
5350
+ });
5351
+
5352
+ <span class="cmt">// Your model, your key. The grid makes no network call — it awaits this.</span>
5353
+ <span class="cmt">// This mock returns one grounded figure (3 rows) and one invented one (900%).</span>
5354
+ <span class="kw">const</span> ask = <span class="kw">async</span> () =&gt; ({ text: 'There are 3 rows in view. Confidence 900%.' });
5355
+
5356
+ <span class="kw">const</span> ai = createAI(grid, { ask, tools: <span class="kw">false</span> });
5357
+ <span class="kw">const</span> result = <span class="kw">await</span> ai.explain({ kind: 'view' });
5358
+
5359
+ <span class="kw">const</span> kept = result.text.includes('3 rows'); <span class="cmt">// grounded — survives</span>
5360
+ <span class="kw">const</span> stripped = !result.text.includes('900%'); <span class="cmt">// hallucinated — removed</span>
5361
+
5362
+ ai.destroy();
5363
+ grid.destroy();
5364
+ <span class="kw">return</span> `${result.flagged.length} flagged | ${kept} kept | ${stripped} stripped`;</code></pre>
5365
+
5271
5366
  <h2 id="mocksocket">The mock socket</h2>
5272
5367
  <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
5368
  <pre><code>import { MockWebSocket, opsFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
@@ -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.40.0</p>
440
+ <p class="rail__sub">Developer guide · v1.43.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.40.0, type declarations
2
+ * Lattice Grid 1.43.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -8000,3 +8000,214 @@ declare module 'lattice-grid/modules/kpi' {
8000
8000
  export function createKPI(el: HTMLElement | null, config?: KPIConfig): KPI;
8001
8001
  export default createKPI;
8002
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'`, `'query'`/`'ask'`. All on when omitted. */
8087
+ enable?: string[];
8088
+ /**
8089
+ * Ask-your-data: apply a safe (read-only) query result without a confirm
8090
+ * step. Off by default — the resolved query is shown and waits for Apply.
8091
+ */
8092
+ autoApply?: boolean;
8093
+ /**
8094
+ * A Data Router instance; on applying a query the answer rows are fanned to
8095
+ * its attached viewers (grid + chart + KPI together) via `load()`.
8096
+ */
8097
+ router?: unknown;
8098
+ /** Budgets passed to the schema builder for ask-your-data. */
8099
+ schemaOptions?: object;
8100
+ /** Extra context passed through to `ask()`. */
8101
+ context?: unknown;
8102
+ /** Called with each ask-your-data result. */
8103
+ onQuery?: (result: AIQueryResult) => void;
8104
+ /** Cap on rows any tool result carries to `ask()`. */
8105
+ maxRows?: number;
8106
+ /** Columns whose values must never leave the browser. */
8107
+ redact?: string | string[] | ((colId: string) => boolean);
8108
+ /** Force tool-use on or off; auto-detected from how `ask` was supplied otherwise. */
8109
+ tools?: boolean;
8110
+ /** Locale for figure formatting. */
8111
+ locale?: string;
8112
+ /** Column cap for a view summary. */
8113
+ maxColumns?: number;
8114
+ /** What to do with an ungrounded figure: `'strip'` (default) or `'flag'`. */
8115
+ reconcile?: 'strip' | 'flag';
8116
+ /** An element to mount the insights panel into. */
8117
+ element?: HTMLElement;
8118
+ /** Called when a narrative is produced. */
8119
+ onNarrative?: (result: AINarrative) => void;
8120
+ /** Called when `ask()` errors; the grid stays usable. */
8121
+ onError?: (error: { error: unknown; target: AITarget }) => void;
8122
+ }
8123
+
8124
+ /** The report from applying an ask-your-data query. */
8125
+ interface AIApplyReport {
8126
+ ok: boolean;
8127
+ /** The action types that were applied. */
8128
+ applied: string[];
8129
+ /** Actions that threw while applying. */
8130
+ failed: Array<{ type: string; reason: string }>;
8131
+ /** Actions refused by the read-only gate — a mutation is never applied. */
8132
+ refused: Array<{ type: string; reason: string }>;
8133
+ /** How many answer rows were fanned to a router's viewers. */
8134
+ fannedOut: number;
8135
+ }
8136
+
8137
+ /**
8138
+ * The result of an ask-your-data question (BACKLOG-0000966): a validated,
8139
+ * READ-ONLY query spec — never rows — that the host reviews before applying.
8140
+ */
8141
+ interface AIQueryResult {
8142
+ /** True when the spec is safe to apply: at least one read, nothing unsafe. */
8143
+ ok: boolean;
8144
+ /** The user's question. */
8145
+ question: string;
8146
+ /** The core plan (from `grid.ai.plan`). */
8147
+ plan: Record<string, unknown>;
8148
+ /** The read-only actions that will run — the validated query spec. */
8149
+ actions: object[];
8150
+ /** Actions refused as not read-only (a mutation the model asked for). */
8151
+ unsafe: Array<{ type: string; reason: string }>;
8152
+ /** Parts the core validator dropped (unknown column, bad operator, …). */
8153
+ rejected: Array<{ at: string; what: string; reason: string }>;
8154
+ /** The model's own one-line summary, if any. */
8155
+ explain: string;
8156
+ /** The validated query spec as data. */
8157
+ spec: { actions: object[] };
8158
+ /** The apply report once applied, or null. */
8159
+ applied: AIApplyReport | null;
8160
+ /** The resolved query in one human sentence, from the validated spec. */
8161
+ describe(): string;
8162
+ /** Apply the query (re-gated), fanning the answer to a router if configured. */
8163
+ apply(opts?: { router?: unknown; onResult?: (rows: object[]) => void }): AIApplyReport;
8164
+ }
8165
+
8166
+ /**
8167
+ * An AI narrative / insights controller over a live grid. Read-only: it
8168
+ * explains the grid's computed figures and answers questions with validated
8169
+ * query specs, and never mutates data. `grid.ai` (in core) is the
8170
+ * complementary intent/plan skill layer this consumes.
8171
+ */
8172
+ interface AI {
8173
+ /** The mounted insights panel element, or null. */
8174
+ readonly el: HTMLElement | null;
8175
+ /** Whether a usable `ask()` is configured. */
8176
+ readonly ready: boolean;
8177
+ /** Produce a grounded, reconciled narrative for a target. */
8178
+ explain(target?: AITarget, opts?: object): Promise<AINarrative>;
8179
+ /** An alias for {@link AI.explain}. */
8180
+ narrate(target?: AITarget, opts?: object): Promise<AINarrative>;
8181
+ /** Mount (or re-target) the insights panel into an element. */
8182
+ insights(el?: HTMLElement, opts?: object): AI;
8183
+ /** Build an "Explain" button bound to a target. */
8184
+ attachExplain(target: AITarget, opts?: object): HTMLElement | null;
8185
+ /** Build the facts packet for a target without calling `ask()`. */
8186
+ facts(target?: AITarget, opts?: object): AIFactsPacket;
8187
+ /**
8188
+ * Ask-your-data: turn a question into a validated, read-only query spec, run
8189
+ * it in the engine, and (on apply) fan the answer to router-attached viewers.
8190
+ * Returns a result the host reviews; `autoApply` applies a safe read for you.
8191
+ */
8192
+ query(question: string, opts?: {
8193
+ autoApply?: boolean; router?: unknown; schemaOptions?: object;
8194
+ context?: unknown; tools?: boolean; signal?: AbortSignal;
8195
+ onResult?: (rows: object[]) => void;
8196
+ }): Promise<AIQueryResult>;
8197
+ /** Apply a reviewed query result (the confirm path); re-gated at the seam. */
8198
+ applyQuery(result: AIQueryResult, opts?: { router?: unknown; onResult?: (rows: object[]) => void }): AIApplyReport;
8199
+ /** Mount the ask-your-data bar (input, Ask, auto-apply toggle, preview, Apply/Discard). */
8200
+ askBar(el?: HTMLElement, opts?: object): AI;
8201
+ on(name: 'narrative' | 'query' | 'error' | string, fn: (payload: object) => void): () => void;
8202
+ off(name: string, fn: (payload: object) => void): void;
8203
+ destroy(): void;
8204
+ }
8205
+
8206
+ /**
8207
+ * Create an AI narrative / insights controller over a live grid. The grid may
8208
+ * be headless or rendered; the module grounds every figure on the grid's
8209
+ * engine and calls only the host's `ask()`.
8210
+ */
8211
+ export function createAI(grid: unknown, config?: AIConfig): AI;
8212
+ export default createAI;
8213
+ }