Lattice Grid Buy a licence

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.

MethodReturnsDescription
schema(opts?)objectThe schema: columns, types, permitted operators.
prompt(text, opts?)stringThe message to send, schema included.
plan(reply, opts?)objectParse 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)objectApplies 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);
MethodReturnsDescription
snapshot()objectEverything, in one structure.
renders()objectCounts by cause, the last render's phase timings, DOM write counters and viewport state.
store()objectPer-column backing kind and byte footprint, total bytes, rows against physical slots, tombstoned rows.
operations()objectCount, mean and worst per operation kind, with a bounded sample of recent calls.
providers()objectCalls, errors, in-flight count and latency per provider, with failures retained.
events()objectListener 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)voidHide a warning for this session. Not permanently.
bundle()objectA support bundle. Contains no row data.
checkOptions(options)booleanTrue when an options object changed identity without its contents changing.
record(kind, detail)voidRecord your own operation, so custom work appears alongside the grid's.
reset()voidZero 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.

IdWhat it means
options-identity-churnAn options object rebuilt on every parent render. A wrapper comparing by identity will tear the grid down each time.
duplicate-row-keysTwo rows share a key. Presents as "the wrong row updated", never as an error.
query-references-unknown-columnA filter or sort names a column that does not exist. Silently matches nothing.
listener-count-growingProbable subscription leak in the host. Presents as gradual slowdown.
main-thread-eligible-for-workerA large operation ran on the main thread despite the worker threshold.
slow-providerA 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'
MethodReturnsDescription
set(key)objectInstall 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()booleanWhether the trial mark is showing.
readyPromiseSettles 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.

HostNo keyKey for *.acme.com
localhost, 127.0.0.1, ::1, *.localhosteverything, no markeverything, no mark
app.acme.comeverything, trial watermarkeverything, no mark
acme.comeverything, trial watermarkeverything, no mark
other.example.orgeverything, trial watermarkeverything, 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.

GroupFunctions
MathsSUM, AVERAGE/AVG, MIN, MAX, COUNT, PRODUCT, ABS, SQRT, POWER, MOD
RoundingROUND, ROUNDUP, ROUNDDOWN, FLOOR, CEILING
LogicIF, AND, OR, NOT, COALESCE
TextCONCAT, LEN, UPPER, LOWER, TRIM, LEFT, RIGHT
StatisticsMEDIAN, 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 =.