Inside Bilig’s verification path for spreadsheet agents

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.11on 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 --jsonThe 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, 24000 → 38400, afterRestore: 38400, and
verified: true. The grid can present that result to a person, but the proof
does not depend on the screen.