Architecture

How it’s built

Static multi-page shell + one client app. No bundler. Layered scripts under a single Metagrid namespace with registries for expansion. Network is optional.

Design goals (v0.1)

Repository layout

projects/metagrid/
  app.html                 spreadsheet shell (DOM + overlays)
  site.css                 marketing + shared
  js/
    core/namespace.js      Metagrid root, hooks, registries
    model/
      structures.js        factories, geometry, get/setCell, TYPE
      state.js             workbook / UI / cloud state
      columns.js           colStruct / rowStruct / lock templates
      history.js           clone, snapshot, undo stack
      seed.js              demo workbook
      pivot.js             pivot structure + fields pane
      chart.js             chart structure + source bind
      access.js            users, nested groups, see/edit/grant
      push.js              button click: snapshot source → write dest
    formula/
      engine.js            tokenize, resolve, eval, special forms
      functions/builtins.js  Metagrid.formula.register(…)
      functions/analysis.js  FILTER, GROUPBY, stats
    api/
      core.js              Agent API: JSON view, named paths, ops
    format/
      document.js          metagrid-v2 export / migrate
      number-format.js     Excel-like number/date display codes
      import-xlsx.js       Excel/CSV → structures
      import-json.js       Plain JSON paste/import → list/table/grid
      calc.js              deps + invalidation
    formula/
      engine.js, builtins, analysis, slash.js  (= pull · / push)
    ui/
      dom.js, ribbon.js, paint.js, selection.js, navigation.js,
      edit.js, commands.js, access.js, dialogs.js, fn-library.js,
      picker.js, keytips.js, keyboard.js, cloud.js, metasheet.js, menus.js,
      chart.js, queries.js, files.js
    boot.js                restore local | seed → ribbon → render → cloud
static no bundler registries layered modules

The Metagrid namespace

Metagrid.version
Metagrid.hooks          on / once / emit / clear
Metagrid.formula        register / get / call / sections / all
Metagrid.commands       register / run / get / all
Metagrid.structures     register / get / typeMap / all
Metagrid.state           live workbook + UI fields
Metagrid.model / .format / .ui
Metagrid.extend         { function, command, structure, hook }

Classic scripts still use shared top-level bindings (root, evalFormula, …) for call-site compatibility. New code should prefer Metagrid.*.

How to extend

Add a formula function

// js/formula/functions/builtins.js (or a new file loaded after it)
Metagrid.formula.register({
  name: "ABS",
  category: "math",       // math | stat | logic | lookup | lambda | text | info
  sig: "ABS(number)",
  args: ["number"],
  desc: "Absolute value (maps over lists)",
  map: true,              // element-wise over lists / columns
  minArgs: 1,
  maxArgs: 1,
  impl(args, ctx) {
    return Math.abs(ctx.asNum(args[0]));
  },
});

The Function library UI is derived from the registry — no second catalog. Special forms (IF, LAMBDA, LET, …) stay in engine.js (custom parse / deferred args) and are registered with special: true for the library list only.

Add a ribbon command

Metagrid.commands.register({
  id: "mycommand",
  run() { /* … */ },
});
// reference id from RIBBON config in ui/ribbon.js

Add a structure type

Metagrid.structures.register({
  type: "grid",
  name: "Grid",
  icon: "⊞",
  color: "var(--sel)",
  mk: (rows, cols) => mkGrid(rows, cols),
});

Built-ins: grid, table, list, pivot, chart. Paint/edit still branch on k.type; the registry is the place to grow new kinds.

Listen for lifecycle

Metagrid.hooks.on("boot", () => { … });
Metagrid.hooks.on("documentLoaded", ({ root, source }) => { … });
Metagrid.hooks.on("afterCommitEdit", ({ k, r, c, val }) => { … });
Metagrid.hooks.on("afterInvalidate", () => { … });

Layers

  1. Core — namespace, registries, hooks, ERR.
  2. Model — structures, geometry, column templates, history, seed.
  3. Formula — tokenizer, path resolve, element-wise map, eval + function registry.
  4. Format — metagrid-v2 export/load, Excel import, calc invalidation.
  5. UI — ribbon, virtualized paint, edit, commands, cloud chrome.
  6. Boot — local restore or demo seed → catalog → ribbon → render → cloud init → hooks.

Agent API

Language models talk to cloud sheets through POST /api/metagrid (api/metagrid.js). Document ops are DOM-free in js/api/core.js (JSON view + named paths). Auth is an owner API key hashed in subsheet_api_keys; RPCs in supabase/migrations/002_api_keys.sql. This is not the sharing/projection layer — keys see the canonical tree of sheets you own. Catalog: GET the same URL with no body. Docs: Agent API.

Formula evaluation path

displayVal(k,r,c)
  → effectiveFormula (cell "=" or table colFormula)
  → computeVal / cellValue
       · noteCellRead → dependency edge into calcDeps
       · visiting set → #CYCLE
       · evalFormula(tokenize(src))
            special forms: IF, LAMBDA, LET, …
            else Metagrid.formula.call(name, args, fctx)
                 · map:true → element-wise over lists
            operators ^ * / + - & cmp also map over lists
  → cache under cellKey = id|r|c

Column structure subsystem

Tables store per-column maps instead of inventing a parallel type system:

FieldRole
colFormulaScalar formula string per column
colStructNested type for every cell in the column
colStructLockBoolean: template owns headers + formulas
colStructHeadersCanonical header list for nested tables
colStructFormulasCanonical colFormula map for nested tables
colValidValidation rule per column

applyLockedTemplateToTable is the single apply path for “parent template → child table.”

Rendering

Persistence

Load order

Documented in app.html comments. Rule of thumb: core → model → formula → push → format → ui → boot. Never call UI from model. Formula may read model; push snapshots formula results then writes stored cells; format may use both; UI uses everything.