@toclocoinc/lattice-grid 1.43.0 → 1.44.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 +62 -2
  3. package/docs/api-detail.html +1 -1
  4. package/lattice-grid.d.ts +131 -8
  5. package/lattice-grid.esm.min.js +25 -12
  6. package/lattice-grid.min.cjs +25 -12
  7. package/lattice-grid.min.js +25 -12
  8. package/modules/ai.esm.min.js +610 -13
  9. package/modules/ai.min.cjs +610 -13
  10. package/modules/ai.min.js +610 -13
  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 +25 -12
  48. package/modules/htmx.min.cjs +25 -12
  49. package/modules/htmx.min.js +25 -12
  50. package/modules/kanban.esm.min.js +16 -13
  51. package/modules/kanban.min.cjs +16 -13
  52. package/modules/kanban.min.js +16 -13
  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 +25 -12
  69. package/modules/webcomponent.min.cjs +25 -12
  70. package/modules/webcomponent.min.js +25 -12
  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.43.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.44.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
package/docs/API.html CHANGED
@@ -2533,6 +2533,66 @@ const writes = createAI(grid, { ask: async () => ({ actions: [{ type: 'setCel
2533
2533
  const write = await writes.query('change the data');
2534
2534
  return `${rows} rows; write ${write.ok ? 'allowed' : 'refused'}`;</code></pre>
2535
2535
 
2536
+ <h3 id="ai-actor">AI as a governed actor (<code>modules/ai</code>, writes)</h3>
2537
+ <p>The governed actor lets the model <strong>propose</strong> edits &mdash; a single NL-targeted
2538
+ change (&ldquo;set the Network Upgrade project to In&nbsp;Progress&rdquo;) or a bulk cleanup &mdash;
2539
+ that a human previews as a <strong>before/after diff</strong> and approves. The model NEVER writes.
2540
+ On approval the edit applies through the grid's <em>own</em> gate, exactly like a person's edit:
2541
+ a grid cell edit via <code>grid.edit.setCells(writes, 'cell', { origin: 'ai' })</code> (the
2542
+ <code>beforeEdit</code> veto), a Kanban card move via <code>board.move(&hellip;, { origin: 'ai' })</code>
2543
+ (the <code>beforeMove</code> veto). <strong>The AI can never bypass the gate:</strong> a host
2544
+ <code>beforeEdit</code>/<code>beforeMove</code> handler that calls <code>preventDefault()</code>
2545
+ (or vetoes async) stops the write, and nothing persists. Writes carry <code>origin: 'ai'</code>
2546
+ on the before-event payload, so a host can allow a person's edit while vetoing the AI's &mdash;
2547
+ policing AI writes distinctly. Approved edits are optimistic and revert on a source reject
2548
+ through the shipped write-back (there is no separate <code>beforeCommit</code> event; the edit gate
2549
+ is <code>beforeEdit</code>, and the optimistic/revert half is <code>grid.edit.settle</code>).</p>
2550
+ <p>NL targeting and any bulk edit bind to the <strong>current filtered view</strong>
2551
+ (<code>grid.rows.data()</code>), never the whole table implicitly; a target not in the view is
2552
+ surfaced for an explicit opt-in <code>widen</code>, not edited silently. A human label is resolved
2553
+ to the column's stored option value; an unknown label is rejected, not coerced. An ambiguous match
2554
+ (more than one row) surfaces its candidates for the user to pick.</p>
2555
+ <div class="table-wrap">
2556
+ <table>
2557
+ <thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
2558
+ <tbody>
2559
+ <tr><td class="sig">propose(instruction, opts?)</td><td class="type">Promise&lt;proposal&gt;</td><td class="desc">Ask the model for structured edits, validate + resolve them against the current view, and return a reviewable proposal with a before/after <code>diff</code>. Nothing is written. <code>opts.widen</code> opts into the full dataset; <code>opts.board</code> routes a Kanban move.</td></tr>
2560
+ <tr><td class="sig">applyProposal(proposal, opts?)</td><td class="type">Promise&lt;report&gt;</td><td class="desc">Apply an approved proposal through the gate (<code>origin: 'ai'</code>). A vetoing before-handler stops it; the report gives <code>applied</code>/<code>vetoed</code>.</td></tr>
2561
+ <tr><td class="sig">actorBar(el, opts?)</td><td class="type">controller</td><td class="desc">Mount the propose &rarr; review-diff &rarr; approve bar, stating the scope.</td></tr>
2562
+ </tbody>
2563
+ </table>
2564
+ </div>
2565
+ <pre data-run="js" data-expect="diff To do then In Progress; vetoed kept todo; approved wrote doing" data-covers="export:createAI"><code>const { createHeadlessGrid } = await import('../packages/core/src/index.js');
2566
+ const { createAI } = await import('../packages/modules/ai/index.js');
2567
+ const status = { options: [{ id: 'todo', label: 'To do' }, { id: 'doing', label: 'In Progress' }] };
2568
+ const seed = () =&gt; createHeadlessGrid({
2569
+ rowKey: 'id',
2570
+ columns: [{ field: 'id' }, { field: 'name' }, { field: 'status', lookup: status, edit: { enabled: true } }],
2571
+ rows: [{ id: 'r1', name: 'Network Upgrade', status: 'todo' }, { id: 'r2', name: 'Payroll', status: 'todo' }],
2572
+ });
2573
+ // A MOCK ask() returns a structured PROPOSAL — never a write, never rows.
2574
+ const ask = async () =&gt; ({ structured: { edits: [{ match: 'Network Upgrade', column: 'status', value: 'In Progress' }] } });
2575
+
2576
+ // Propose: a before/after diff, nothing written yet.
2577
+ let grid = seed();
2578
+ let ai = createAI(grid, { ask });
2579
+ const proposal = await ai.propose('set the Network Upgrade project to In Progress');
2580
+ const d = proposal.diff[0];
2581
+ const before = `${d.oldDisplay} then ${d.newDisplay}`;
2582
+
2583
+ // A vetoing beforeEdit handler on the AI write => NOTHING persists.
2584
+ grid.on('beforeEdit', (e) =&gt; { if (e.origin === 'ai') e.preventDefault('reviewed elsewhere'); });
2585
+ await ai.applyProposal(proposal);
2586
+ const vetoed = grid.rows.byKey('r1').data.status;
2587
+
2588
+ // Approve on a grid with no veto => the edit lands through the gate.
2589
+ grid = seed();
2590
+ ai = createAI(grid, { ask });
2591
+ await ai.applyProposal(await ai.propose('set the Network Upgrade project to In Progress'));
2592
+ const approved = grid.rows.byKey('r1').data.status;
2593
+
2594
+ return `diff ${before}; vetoed kept ${vetoed}; approved wrote ${approved}`;</code></pre>
2595
+
2536
2596
  <h2 id="overlay">grid.overlay</h2>
2537
2597
  <div class="table-wrap">
2538
2598
  <table>
@@ -7693,7 +7753,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7693
7753
  <tr><td class="name">stop</td><td class="type">(cancel?: boolean): void</td><td class="desc"></td></tr>
7694
7754
  <tr><td class="name">undo</td><td class="type">(): void</td><td class="desc"></td></tr>
7695
7755
  <tr><td class="name">redo</td><td class="type">(): void</td><td class="desc"></td></tr>
7696
- <tr><td class="name">setCells</td><td class="type">(writes: { key: string; colId: string; value: unknown }[], type?: 'cell' | 'fill' | 'paste'): number</td><td class="desc"></td></tr>
7756
+ <tr><td class="name">setCells</td><td class="type">(</td><td class="desc">Write several cells as one undoable step (§12). `opts.origin` defaults to `'api'` the ungated seam every existing caller uses (a fill, a paste, a kanban move), unchanged. Pass `{ origin: 'ai' }` (or `'user'`) to route the write through the cancellable `beforeEdit` gate, exactly as an interactive edit is (BACKLOG-0000967): the AI writes through this so a host `beforeEdit` handler can veto it and nothing persists when it does. With a gated origin and an async (deferring) before-handler, the return is a `Promise&lt;number&gt;`.</td></tr>
7697
7757
  <tr><td class="name">bulkSet</td><td class="type">(value: unknown, opts?: { cells?: { key: string; colId: string }[] }): number</td><td class="desc">Set one value across a block of cells as a single undoable step (§12, card 740). Defaults to the selected range; read-only and non-editable cells are skipped and every write runs the normal parse/validate path.</td></tr>
7698
7758
  <tr><td class="name">fill</td><td class="type">(opts?: { direction?: 'down' | 'up' | 'left' | 'right'; series?: boolean; range?: CellRange }): number</td><td class="desc">Fill a selected range from its leading edge as one undoable step (§12, card 740). The default copies the anchor across the range (Excel's Ctrl+D and its natural siblings); `series: true` extrapolates a numeric or date series from the first one or two cells of each line, falling back to a copy for types with no series. `direction` defaults to `'down'`.</td></tr>
7699
7759
  <tr><td class="name">pasteInto</td><td class="type">(anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): number</td><td class="desc"></td></tr>
@@ -8237,7 +8297,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8237
8297
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8238
8298
  <tbody>
8239
8299
  <tr><td class="name">type</td><td class="type">string</td><td class="desc"></td></tr>
8240
- <tr><td class="name">origin</td><td class="type">'api' | 'user' | 'init'</td><td class="desc"></td></tr>
8300
+ <tr><td class="name">origin</td><td class="type">'api' | 'user' | 'init' | 'ai'</td><td class="desc">Who caused the action. `'ai'` (BACKLOG-0000967) tags a write an AI proposed and a human approved, applied through `grid.edit.setCells(writes, type, { origin: 'ai' })`; it fires the same cancellable `beforeEdit` gate a `'user'` edit does, so a host can policy-gate AI writes distinctly.</td></tr>
8241
8301
  <tr><td class="name">grid</td><td class="type">Grid</td><td class="desc"></td></tr>
8242
8302
  </tbody>
8243
8303
  </table>
@@ -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.43.0</p>
440
+ <p class="rail__sub">Developer guide · v1.44.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.43.0, type declarations
2
+ * Lattice Grid 1.44.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -3772,7 +3772,13 @@ export type EventName =
3772
3772
 
3773
3773
  export interface GridEvent {
3774
3774
  type: string;
3775
- origin: 'api' | 'user' | 'init';
3775
+ /**
3776
+ * Who caused the action. `'ai'` (BACKLOG-0000967) tags a write an AI proposed
3777
+ * and a human approved, applied through `grid.edit.setCells(writes, type,
3778
+ * { origin: 'ai' })`; it fires the same cancellable `beforeEdit` gate a
3779
+ * `'user'` edit does, so a host can policy-gate AI writes distinctly.
3780
+ */
3781
+ origin: 'api' | 'user' | 'init' | 'ai';
3776
3782
  grid: Grid;
3777
3783
  [key: string]: unknown;
3778
3784
  }
@@ -4101,7 +4107,20 @@ export interface EditApi {
4101
4107
  stop(cancel?: boolean): void;
4102
4108
  undo(): void;
4103
4109
  redo(): void;
4104
- setCells(writes: { key: string; colId: string; value: unknown }[], type?: 'cell' | 'fill' | 'paste'): number;
4110
+ /**
4111
+ * Write several cells as one undoable step (§12). `opts.origin` defaults to
4112
+ * `'api'` — the ungated seam every existing caller uses (a fill, a paste, a
4113
+ * kanban move), unchanged. Pass `{ origin: 'ai' }` (or `'user'`) to route the
4114
+ * write through the cancellable `beforeEdit` gate, exactly as an interactive
4115
+ * edit is (BACKLOG-0000967): the AI writes through this so a host `beforeEdit`
4116
+ * handler can veto it and nothing persists when it does. With a gated origin
4117
+ * and an async (deferring) before-handler, the return is a `Promise<number>`.
4118
+ */
4119
+ setCells(
4120
+ writes: { key: string; colId: string; value: unknown }[],
4121
+ type?: 'cell' | 'fill' | 'paste',
4122
+ opts?: { origin?: 'api' | 'ai' | 'user' },
4123
+ ): number | Promise<number>;
4105
4124
  /**
4106
4125
  * Set one value across a block of cells as a single undoable step (§12, card
4107
4126
  * 740). Defaults to the selected range; read-only and non-editable cells are
@@ -8101,6 +8120,14 @@ declare module 'lattice-grid/modules/ai' {
8101
8120
  context?: unknown;
8102
8121
  /** Called with each ask-your-data result. */
8103
8122
  onQuery?: (result: AIQueryResult) => void;
8123
+ /** Called with each governed-actor proposal (Play C), before any approval. */
8124
+ onProposal?: (result: AIProposal) => void;
8125
+ /**
8126
+ * A Kanban board (from `createKanban`) the governed actor writes moves
8127
+ * through: an NL card move applies via the board's own `beforeMove` gate
8128
+ * (BACKLOG-0000967), never a kanban-specific write bypass.
8129
+ */
8130
+ board?: unknown;
8104
8131
  /** Cap on rows any tool result carries to `ask()`. */
8105
8132
  maxRows?: number;
8106
8133
  /** Columns whose values must never leave the browser. */
@@ -8163,11 +8190,82 @@ declare module 'lattice-grid/modules/ai' {
8163
8190
  apply(opts?: { router?: unknown; onResult?: (rows: object[]) => void }): AIApplyReport;
8164
8191
  }
8165
8192
 
8193
+ /** One before/after change in a governed-actor proposal (BACKLOG-0000967). */
8194
+ interface AIDiffEntry {
8195
+ /** The target row key. */
8196
+ key: string;
8197
+ /** A human label identifying the row (a name-like column, else the key). */
8198
+ rowLabel: string;
8199
+ /** The target column id. */
8200
+ colId: string;
8201
+ /** The column's title, for the diff header. */
8202
+ colTitle: string;
8203
+ /** The current stored value. */
8204
+ oldValue: unknown;
8205
+ /** The current value as shown (a lookup id mapped to its label). */
8206
+ oldDisplay: string;
8207
+ /** The proposed stored value (a label resolved to its option id). */
8208
+ newValue: unknown;
8209
+ /** The proposed value as shown. */
8210
+ newDisplay: string;
8211
+ }
8212
+
8166
8213
  /**
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.
8214
+ * A governed-actor proposal (Play C, BACKLOG-0000967): the model's structured
8215
+ * edits, VALIDATED and resolved against the current view never written until
8216
+ * a human approves. `apply()` writes ONLY through the grid's own gate.
8217
+ */
8218
+ interface AIProposal {
8219
+ /** True when there is at least one applicable change and nothing needs a pick first. */
8220
+ ok: boolean;
8221
+ /** The user's instruction. */
8222
+ instruction: string;
8223
+ /** `'view'` (the filtered set, the default) or `'all'` (an opted-in widen). */
8224
+ scope: 'view' | 'all';
8225
+ /** How many rows the scope covers. */
8226
+ scopeCount: number;
8227
+ /** The scope in words, always stated in the confirm/diff. */
8228
+ scopeText: string;
8229
+ /** Whether any proposal was a bulk (`scope:'view'`) edit. */
8230
+ bulk: boolean;
8231
+ /** The before/after diff — exactly what would change. Nothing is written yet. */
8232
+ diff: AIDiffEntry[];
8233
+ /** Proposals refused before apply (unknown column, unknown label, bad type/range, no match). */
8234
+ rejected: Array<{ reason: string; [k: string]: unknown }>;
8235
+ /** Matches needing a human pick (>1 row for one phrase), with candidates. */
8236
+ ambiguous: Array<{ reason: string; candidates: Array<{ key: string; label: string }>; [k: string]: unknown }>;
8237
+ /** Named targets found only outside the view, offered for an opt-in widen. */
8238
+ outOfView: Array<{ reason: string; candidates: Array<{ key: string; label: string }>; [k: string]: unknown }>;
8239
+ /** Matches whose value already equals the ask (nothing to change). */
8240
+ noops: Array<{ reason: string; [k: string]: unknown }>;
8241
+ /** The apply report once applied, or null. */
8242
+ applied: AIProposalReport | null;
8243
+ /** The proposal in one human sentence, always stating the scope. */
8244
+ describe(): string;
8245
+ /** Apply the approved diff through the gate (`beforeEdit`, or `beforeMove` for a board). */
8246
+ apply(opts?: { board?: unknown }): Promise<AIProposalReport>;
8247
+ }
8248
+
8249
+ /** The report from applying a governed-actor proposal. */
8250
+ interface AIProposalReport {
8251
+ /** True when at least one edit landed. */
8252
+ ok: boolean;
8253
+ /** How many edits landed through the gate. */
8254
+ applied: number;
8255
+ /** How many edits were attempted. */
8256
+ requested: number;
8257
+ /** How many were stopped by a before-handler veto. */
8258
+ vetoed: number;
8259
+ /** Which gated path applied them: `'setCells'`, `'board.move'`, or `'none'`. */
8260
+ via: string;
8261
+ }
8262
+
8263
+ /**
8264
+ * An AI controller over a live grid. It explains the grid's computed figures
8265
+ * (Play A), answers questions with validated read-only query specs (Play B),
8266
+ * and PROPOSES governed edits a human approves and the grid's own gate applies
8267
+ * (Play C). `grid.ai` (in core) is the complementary intent/plan skill layer
8268
+ * this consumes.
8171
8269
  */
8172
8270
  interface AI {
8173
8271
  /** The mounted insights panel element, or null. */
@@ -8198,7 +8296,32 @@ declare module 'lattice-grid/modules/ai' {
8198
8296
  applyQuery(result: AIQueryResult, opts?: { router?: unknown; onResult?: (rows: object[]) => void }): AIApplyReport;
8199
8297
  /** Mount the ask-your-data bar (input, Ask, auto-apply toggle, preview, Apply/Discard). */
8200
8298
  askBar(el?: HTMLElement, opts?: object): AI;
8201
- on(name: 'narrative' | 'query' | 'error' | string, fn: (payload: object) => void): () => void;
8299
+ /**
8300
+ * Governed actor (Play C): ask the model for structured edit PROPOSALS over
8301
+ * the current view, validate and resolve them (label -> stored value, locate
8302
+ * a named row, reject unknown columns/labels/out-of-range), and return a
8303
+ * reviewable {@link AIProposal} with a before/after diff. NOTHING is written
8304
+ * — the model proposes; a human approves.
8305
+ */
8306
+ propose(instruction: string, opts?: {
8307
+ widen?: boolean; board?: unknown; schemaOptions?: object; maxRows?: number;
8308
+ context?: unknown; redact?: string | string[] | ((colId: string) => boolean);
8309
+ signal?: AbortSignal;
8310
+ }): Promise<AIProposal>;
8311
+ /**
8312
+ * Apply an approved proposal — the human-approval step. Writes ONLY through
8313
+ * the gate: a grid cell edit via `grid.edit.setCells({ origin: 'ai' })` (the
8314
+ * `beforeEdit` veto), a kanban move via `board.move({ origin: 'ai' })` (the
8315
+ * `beforeMove` veto). A vetoing host handler stops the write.
8316
+ */
8317
+ applyProposal(result: AIProposal, opts?: { board?: unknown }): Promise<AIProposalReport>;
8318
+ /**
8319
+ * Mount the governed-actor bar: an instruction input, Propose, a before/after
8320
+ * diff preview stating the scope, and Approve/Discard. Approve applies
8321
+ * through the gate.
8322
+ */
8323
+ actorBar(el?: HTMLElement, opts?: object): AI;
8324
+ on(name: 'narrative' | 'query' | 'proposal' | 'error' | string, fn: (payload: object) => void): () => void;
8202
8325
  off(name: string, fn: (payload: object) => void): void;
8203
8326
  destroy(): void;
8204
8327
  }