api reference
The model layer, diagnostics and licence
grid.ai and the intent layer, grid.diagnostics, formulas, and grid.licence.
API reference › The model layer, diagnostics and licence
grid.ai
The grid composes a prompt describing its own columns and operators, you send it to whichever model you like, and it validates the reply before anything is applied. It makes no network call and has no default model.
| Method | Returns | Description |
|---|---|---|
| schema(opts?) | object | The schema: columns, types, permitted operators. |
| prompt(text, opts?) | string | The message to send, schema included. |
| plan(reply, opts?) | object | Parse and validate. Unknown columns and operators are rejected with a reason; valid actions in the same reply are kept. plan.describe() renders it in plain English for confirmation. |
| apply(plan) | object | Applies an approved plan as one undo entry, labelled with what it did. |
Actions: setFilters, setSort, groupBy, showColumns, hideColumns, setQuick, clear. Nothing else is executable, so a model cannot be talked into an operation the vocabulary does not contain. docs/AI-SKILL.md is the reference to hand your model.
grid.diagnostics
What the grid is doing, as data: render counts and their causes, memory layout, operation and provider timing, event listener counts, effective configuration, and a list of things that look like mistakes.
This is the API; the devtools panel is a consumer of it. Built in that order deliberately: instrumentation shaped by a UI tends to report what is convenient to display rather than what is true, and an API that only exists behind a panel cannot be asserted against in a test.
// the assertion this exists to make possible
const before = grid.diagnostics.renders().dom.cellWrites;
grid.filters.set({ col: 'status', op: 'eq', value: 'active' });
await nextFrame();
const written = grid.diagnostics.renders().dom.cellWrites - before;
expect(written).toBeLessThan(200);
| Method | Returns | Description |
|---|---|---|
| snapshot() | object | Everything, in one structure. |
| renders() | object | Counts by cause, the last render's phase timings, DOM write counters and viewport state. |
| store() | object | Per-column backing kind and byte footprint, total bytes, rows against physical slots, tombstoned rows. |
| operations() | object | Count, mean and worst per operation kind, with a bounded sample of recent calls. |
| providers() | object | Calls, errors, in-flight count and latency per provider, with failures retained. |
| events() | object | Listener count per event type. |
| config() | object | { effective, supplied, defaulted }, which values you chose and which the grid chose. |
| warnings() | object[] | Everything flagged, newest first, each with a stable id. |
| dismiss(id) | void | Hide a warning for this session. Not permanently. |
| bundle() | object | A support bundle. Contains no row data. |
| checkOptions(options) | boolean | True when an options object changed identity without its contents changing. |
| record(kind, detail) | void | Record your own operation, so custom work appears alongside the grid's. |
| reset() | void | Zero the counters. Warnings and configuration are left alone. |
The support bundle
bundle() returns configuration, query state, timing history, warnings, provider statistics, version and environment. It never contains row data, cell values or column values, and says so in its own contains field. The guarantee is the point: a bundle that had to be read for confidential content before sending is a bundle nobody sends.
Warnings
Each carries a stable id a support conversation can name, a plain description, and the specific values involved. Two sources are merged: checks run against the grid, and everything the grid has reported through its own one-per-cause warnings.
| Id | What it means |
|---|---|
| options-identity-churn | An options object rebuilt on every parent render. A wrapper comparing by identity will tear the grid down each time. |
| duplicate-row-keys | Two rows share a key. Presents as "the wrong row updated", never as an error. |
| query-references-unknown-column | A filter or sort names a column that does not exist. Silently matches nothing. |
| listener-count-growing | Probable subscription leak in the host. Presents as gradual slowdown. |
| main-thread-eligible-for-worker | A large operation ran on the main thread despite the worker threshold. |
| slow-provider | A provider took more than a second to answer. |
The devtools panel
An optional module. It imports nothing (the grid is handed to it) so deployments that never load it pay nothing.
import { createGrid } from '@toclocoinc/lattice-grid';
import { createDevtools } from '@toclocoinc/lattice-grid/modules/devtools';
const grid = createGrid(el, config);
createDevtools({ grid }); // Ctrl+Shift+D collapses it
Nine tabs over the API above, a compact vitals strip to leave open while working, and a render heat overlay that tints cells as they are written: blue for a new value, red for a cell rewritten with the value it already held. The second colour is the one worth chasing: it is work the grid did not need to do, and no counter alone will tell you where it is.
The panel observes and never mutates. A configuration editor would create a second path into state that has to be kept correct forever, so there is not one. Nothing leaves the browser: there is no telemetry, and the bundle is produced only when you ask for it.
grid.licence
There is one Lattice Grid and every copy is feature-identical. No community edition, no pro tier, no feature held back behind a key. A licence removes the trial watermark; that is the whole of what it does.
Free to develop against, licensed to deploy. A grid on localhost, or any loopback host: needs no key at all. On any other domain an unlicensed grid still renders everything and carries a small trial watermark linking to latticegrid.dev. Nothing ever refuses to render: the failure worth avoiding is a broken production screen, and no licensing state is worth causing one.
// Before creating a grid.
LatticeGrid.setLicence('LG1.…'); // your key; setLicense also works
const grid = LatticeGrid.createGrid(el, config);
grid.licence.state(); // 'licensed' | 'localhost' | 'trial'
| Method | Returns | Description |
|---|---|---|
| set(key) | object | Install a key for the process. Returns the provisional verdict; the check is asynchronous and licence:changed fires when it settles. |
| state() | string | 'licensed', 'localhost' or 'trial'. |
| info() | object | { valid, reason, issuedTo, expires, product }. expires is undefined for a perpetual key (the default) and an ISO date only for one deliberately issued with a term. |
| watermark() | boolean | Whether the trial mark is showing. |
| ready | Promise | Settles when the licence check finishes. |
Domains
A key names the hosts it covers. *.acme.com matches app.acme.com,
a.b.acme.com and acme.com itself, a wildcard that refused the
apex would be a puzzle rather than a licence. A bare acme.com matches only itself,
and a key naming no domains is valid anywhere. Matching ignores case and a trailing dot.
| Host | No key | Key for *.acme.com |
|---|---|---|
| localhost, 127.0.0.1, ::1, *.localhost | everything, no mark | everything, no mark |
| app.acme.com | everything, trial watermark | everything, no mark |
| acme.com | everything, trial watermark | everything, no mark |
| other.example.org | everything, trial watermark | everything, trial watermark |
.local, .internal and private IP ranges are not exempt. They are ordinary LAN names, and a corporate intranet is a deployment like any other.
Getting a key
Keys are issued from latticegrid.dev. A key is issued per domain rather than per developer or per seat: name the domains the grid will run on and one key covers every developer, every build and every user on them.
Keys are perpetual by default. A key carries no expiry unless one was deliberately issued (a trial, a time-boxed pilot) so the ordinary key is valid until the domains it names change, not one that quietly lapses on a date nobody is tracking.
Checking a key needs no network. There is no licence server, no call home, and nothing that can fail at three in the morning, a key carries its own answer and the grid reads it locally, fresh on every load. A key for the wrong domain, or a key that will not read at all, does the same thing a genuinely expired trial key does: log one console warning and show the watermark.
Install the key before creating a grid. Setting one later still works,
licence:changed fires and the watermark is removed, but the first frames of the
grid will carry it.
Formulas
A leading = in a numeric cell is a formula. The grid stores what it comes to.
=5 + 5
=quantity * unitPrice
=[Unit Price] * 1.2
=ROUND(quantity * unitPrice, 2)
=IF(quantity > 10, "bulk", "single")
=SUM(readings) // an array property on the row
References name columns of the same row, not cells, a grid sorts, filters,
groups and pages, so A1 would mean a different row from one moment to the next.
Matching is on field or title, ignoring case and spacing; bracket a name that contains spaces.
A property with no column of its own is reachable too.
| Group | Functions |
|---|---|
| Maths | SUM, AVERAGE/AVG, MIN, MAX, COUNT, PRODUCT, ABS, SQRT, POWER, MOD |
| Rounding | ROUND, ROUNDUP, ROUNDDOWN, FLOOR, CEILING |
| Logic | IF, AND, OR, NOT, COALESCE |
| Text | CONCAT, LEN, UPPER, LOWER, TRIM, LEFT, RIGHT |
| Statistics | MEDIAN, PERCENTILE, QUARTILE1, QUARTILE3, IQR, STDEV, STDEVP, VAR, VARP, COUNTDISTINCT |
The statistical functions use R type 7 quantiles, the same definition as the totals row, grid.statistics and the distribution formatting rules, so the four never disagree about what a median is. PERCENTILE reads 90 and 0.9 as the same request. Over an empty set they return a number rather than null, because a formula is arithmetic and has to keep composing.
Operators + - * / ^ with parentheses, comparison for IF, and postfix
%. ^ is right-associative and unary minus binds tighter than it, so
-2^2 is 4: Excel's answer rather than mathematics'.
// Your own functions, on top of the built-in library.
createGrid(el, {
formulaFunctions: {
MARGIN: ([revenue, cost]) => (revenue - cost) / revenue,
},
});
The result is stored, not the expression. A formula is a way
of entering a value: like 1,200, (50) or 12%, and it
commits as one undo step with the column's own validation. Persisting a formula and recalculating
it when a dependency changes is a separate feature; referencesOf() is exported for
anyone building it.
No eval, no new Function. A formula
is text a user typed, so evaluating it with the JavaScript engine would let anyone who can edit a
cell run code in your page. It is a hand-written parser and the only callable things are the
functions above.
Bare arithmetic is not a formula. 2-1 is a
plausible product code and 1/2 a plausible date, so both are refused rather than
guessed at. Declare a formula with =.