Inside Bilig’s verification path for spreadsheet agents

Inside Bilig’s verification path for spreadsheet agents cover image

Bilig starts with a deliberately narrow promise: keep the workbook model, run the rule in Node. The public package, @bilig/workpaper, is aimed at services, tests, and agents that need workbook-shaped calculations. It is not presented as a visual spreadsheet app or a finished Excel clone.

That boundary explains the rest of the project. The browser is useful for people. The model needs a programmable surface. And an agent needs evidence that survives the moment it changes a cell.

The proof output and API example in this article were verified against @bilig/workpaper@0.164.11 on August 30, 2026. Keep that version pinned when reproducing the exact result; a later release may change the output or API. The package requires Node.js 22 or newer.

Start with a small state owner

The first useful Bilig example does not open a spreadsheet window. It builds two sheets, gives one cell a formula, changes an input, and asks for the dependent result.

import { buildA1WorkPaper } from '@bilig/workpaper'
 
const book = buildA1WorkPaper({
  Inputs: [
    ['Metric', 'Value'],
    ['Customers', 20],
    ['Average revenue', 1200],
  ],
  Summary: [
    ['Metric', 'Value'],
    ['Revenue', '=Inputs!B2*Inputs!B3'],
  ],
})
 
const proof = book.editAndReadback('Inputs!B2', 32, {
  readbackRange: 'Summary!B2',
})
 
console.log({
  before: proof.beforeReadback.displayValues,
  after: proof.afterReadback.displayValues,
  afterRestore: proof.restoredReadback.displayValues,
  verified: proof.verified,
})
 
book.dispose()

The numbers are deliberately uninteresting: revenue moves from 24000 to 38400, and the restored document returns 38400 again. That is the point. A tool can return the edited address, the formula-backed readback, and the persistence check as data. It does not need to infer success from a cursor or a screen repaint.

The API also keeps the ordinary operations visible. set, setMany, readMany, display, and saveJson are available when a full proof object is not needed; editManyAndReadback groups several input changes into one readback and restore check. The public package documents those boundaries in its TypeScript API.

Make the first proof runnable

One of the better artifacts in the repository is the package-owned evaluator. From an empty project, a developer can run:

npm exec --yes --package @bilig/workpaper@0.164.11 -- bilig-evaluate \
  --door workpaper-service --json

The service door edits an input, recalculates a dependent cell, serializes the WorkPaper document, restores it, and reports whether the checks passed. The stable part of the output is the relationship between the values:

{
  "editedCell": "Inputs!B2",
  "dependentCell": "Summary!B2",
  "before": 24000,
  "after": 38400,
  "afterRestore": 38400,
  "verified": true
}

There is a second evaluator door for the MCP path. It checks tool discovery, mutation, formula readback, WorkPaper export, persistence, and restart readback. That separation is practical: a service integration and a tool-host integration have different failure surfaces, so they should not hide behind the same smoke test.

Let an incomplete proof stay incomplete

The browser-facing agent path makes the same idea more explicit. Its WorkbookToolMutationReceipt contains the operation, affected ranges, before and after revisions, recalculation proof, authoritative readback, rendered readback, semantic readback, undo information, and warnings.

The status is not a synonym for “the command handler returned.” The application can distinguish a staged change from a queued one, a fully applied change from a failed verification, and an operation whose proof is incomplete. In the applied case, the code requires recalculation, matching authoritative and rendered readbacks, semantic agreement, and an available undo path.

The tests make those distinctions concrete. They cover a claimed write that does not match authoritative state, a rendered range captured at an older revision, and a mutation whose undo proof is missing. Each case leaves the result verification-incomplete instead of turning a plausible screen into a false success.

That is a useful vocabulary for agents. “Applied” answers whether a mutation was executed. “Verified” answers whether the relevant checks passed. “Rendered” answers whether the person’s view caught up. They are related states, not three ways to say the same thing.

Persistence has to be exercised, not promised

It is easy to demonstrate a recalculated value while everything is still in memory. Bilig’s examples carry the model across a serialization boundary. The persistence-roundtrip.ts example writes a WorkPaper JSON document, restores it, edits the restored model, and reads the dependent summary again. The snapshot-diff.ts example compares serialized input before and after a change alongside the calculated summary values.

That extra round trip catches a class of bugs that a direct read misses. A formula may calculate correctly in memory and disappear during serialization. A named expression may not survive restore. A caller may save the wrong document after the edit. Persistence is part of the behavior, so it belongs in the example and in the assertion.

The corpus made “real” measurable

Toy workbooks are good for explaining an API. They are a weak test of a browser runtime. The repository therefore defines a deterministic workbook corpus with seven cases across dense, wide, and multisheet families. The cases include 100,000-cell and 250,000-cell snapshots, frozen panes, variable column widths, cross-sheet formulas, and a ledger viewport. The workbook-corpus.ts definitions and tests make those shapes inspectable and repeatable.

The cell counts are not a performance trophy by themselves. They are a way to make a claim reproducible. A rendering or synchronization change can be tested against the same dense grid, wide grid, or multisheet calculation instead of a hand-built demo that only exercises the happy path.

The browser still has a separate job

A headless model does not make the grid irrelevant. Bilig’s web runtime keeps worker-backed workbook state, projects visible ranges, and renders the sheet as tiles. The projected tile scene store tracks the revisions and residency of those tiles, and the rendered-freshness tests cover the stale-visible-state boundary.

That architecture gives the two audiences what they need. The agent can read the authoritative model. The person can inspect selection, formatting, frozen panes, and the range on screen. If the model is at revision 12 and a visible tile still represents revision 11, the UI has a problem to report—not a reason for the agent to guess.

The same rule applies to import. An .xlsx file is a different contract from a WorkPaper JSON document. Bilig’s documentation keeps import, export, formula support, and compatibility checks explicit rather than implying that a passing headless calculation proves desktop Excel parity.

Keep the claim narrow enough to test

Bilig does not claim every Excel feature. The public compatibility notes call out macros, external links, volatile functions, pivots, charts, collaborative editing, and desktop fidelity as boundaries that may require another runtime or additional review. That is a healthier product statement than “an agent can use Excel.”

What the project can show is more concrete: a typed workbook surface, formula recalculation, explicit cell and range operations, JSON persistence, restore readback, MCP tools, and a browser runtime that treats visible state as its own thing. Those pieces are small enough to exercise independently and strong enough to compose into a service workflow.

The resulting contract is concrete. A verified run names the input that changed, the dependent value that changed with it, the serialized document that survived restore, and the compatibility limits that remain. For the example above, that evidence is Inputs!B2, Summary!B2, 2400038400, afterRestore: 38400, and verified: true. The grid can present that result to a person, but the proof does not depend on the screen.