Skip to content

Releases: lianghsun/open-sheet

v0.1.8 — six rounds of real workbooks

Choose a tag to compare

@lianghsun lianghsun released this 20 Aug 07:40

Twenty-three fixes, most of them found by building real workbooks against the
framework rather than by testing it as a library. Two were broken from the day
they shipped.

Broken since release

Leaving a note from the Inspector broke the workbook — every time. The marker
was written as //, which is a comment in JS but a bare text node in JSX
children, where a <Table> always sits. Every note ever left stopped the workbook
compiling, with an error reading <Stack> got bare text — which looks nothing
like "you left a note". The panel advertises the feature; it is the entry to the
collaboration path.

sheet-authoring/SKILL.md shipped overwritten since 0.1.3. A scripted edit
missed its target and wrote one of the skill's own reference files over it,
taking the file contract, the component surface, and the self-review checklist.
Nothing imports a skill and no build step reads one, so it failed in silence —
the only symptom was worse workbooks written by someone else's agent.

Both now have tests. The note test is named for what it missed:
leaves the file parseable — the test this feature shipped without.

Functions that existed but could not do the thing you wanted them for

  • IFERROR could never catch anything. Arguments were evaluated eagerly and
    any Excel error returned immediately, so it never reached its implementation.
    IF evaluated both branches, failing on exactly the rows its guard existed for.
    All three are lazy now, as Excel evaluates them.
  • SUMPRODUCT did not support arrays. Summing one plain range is just SUM;
    array comparison is the entire reason to reach for it, so the evaluable subset
    was precisely the part nobody uses.
  • ABS, ROUND and friends stopped at a range. They map now. Aggregates
    deliberately do not: MIN over a range is a minimum, not one per cell.
  • raw tagged templates rejected expressions. The signature said Ref | Expr
    and only Ref was implemented, so interpolating r.cell('x') — the commonest
    case — crashed the writer while tsc stayed happy.

Honesty

Anything the evaluator cannot compute now reports #NOT_EVALUATED rather than a
fabricated #VALUE!. That kept an invented Excel error out of the exported cache,
and stops iferror from swallowing a gap in this evaluator as though it were a
real spreadsheet condition.

Number formats honour Excel's sections, so accounting shows (84,500) and -
in the viewer as it does in Excel. The viewer and the workbook disagreeing about
the same cell is the failure a "what you see is what exports" tool can least
afford.

Errors that say what to do

Build failures name the workbook and file. Cycles name the construct — "costs" column "b" row 1 — rather than an internal coordinate the author never wrote.
A formula that fails to resolve names its block, column and row, instead of
leaving you to bisect a 450-line workbook by hand. r.cell() on a field with no
column explains both ways forward and names the tradeoff.

--help no longer runs a build and overwrites dist/.

What the framework cannot check for you

Three real workbooks produced three failures with one shape: every reference
correct, every figure sourced, nothing wrong in any cell — and the result wrong
anyway.

Shape Example
Ordering decided in data a sorted array: right this month, stale next month
Periods of unequal length two days of data beside thirty → +790%
Sources of unequal coverage 30 days of cost ÷ 20 days of requests → wrong by 50%

The framework guarantees referential integrity. It cannot guarantee that the
things being compared are comparable, and that class of error looks identical to
a correct result. sheet-authoring now names it, with the pattern that avoids
the third: normalise each figure by its own coverage before combining them.

Also

Design panel stops misreporting state; the viewer keeps your place across the
reload an edit triggers; comment markers name their block; elementwise mapping,
periods-as-columns, computed columns, and CSV's raw values are documented.

npx @open-sheet/cli init my-sheets

v0.1.2 — the viewer actually renders

Choose a tag to compare

@lianghsun lianghsun released this 20 Aug 02:55

Two patches after the first release, both fixing the same symptom: a blank viewer in every published install.

What was wrong

0.1.0 and 0.1.1 worked from a clone and were broken from npm. The main entry
reached Node-only code — the CLI, the dev server, and the optional
import('playwright') behind PDF export — and a workbook imports that entry and
is evaluated in the browser by the viewer. Under npm's flat node_modules
that handed the browser CommonJS it could not import, and Vite's dependency
optimizer could not analyse the dynamic import at all. The page died before
mounting.

What changed

Node-only exports moved to @open-sheet/core/node. The main entry is
browser-safe: authoring components, formula builders, references, compile,
evaluate, the style model, and the pure exporters. A workbook no longer pulls
exceljs or Vite into the browser.

0.1.1 separately fixed @vitejs/plugin-react being bundled into dist, which
made its Fast Refresh runtime resolve to a file that does not exist.

Why it shipped twice

Both faults existed only in the packaged shape, and both returned HTTP 200 for
every module. The pre-publish rehearsal checked status codes, which proved
nothing about whether anything rendered.

There is now a CI job that packs the packages, installs them into a clean
directory, drives a real browser, and asserts the grid painted formatted values
with an empty console. It is the only check that would have caught either bug.

Upgrading

rm -rf node_modules/.vite
npm install @open-sheet/core@latest

v0.1.0 — first release

Choose a tag to compare

@lianghsun lianghsun released this 19 Aug 05:17

The spreadsheet framework built for agents. First release.

npx @open-sheet/cli init my-sheets

What it does

You author a workbook as JSX with named data columns, and export a .xlsx
containing live formulas — no cell address appears anywhere in the source.
Add a row to the data and every reference re-resolves.

col('grossProfit', { formula: (r) => sub(r.cell('revenue'), r.cell('cogs')) })

exports as =B5-C5, and an assumption referenced by name exports as
=F6*(1-taxRate) against a real Excel defined name — legible to whoever opens
the file, not just to the machine.

In this release

  • Compiler — JSX → measure → place → grid, with the framework owning every coordinate
  • References and formulas — one expression AST with two backends: serialize() writes the Excel formula, evaluate() computes what the viewer shows. Deliberately avoids GPL-licensed engines
  • Viewer — virtualized grid, formula bar showing the resolved formula, inspect mode with source write-back, design panel, themes and assets
  • Agent skills — /create-sheet, /sheet-authoring, /current-sheet, /apply-comments, /create-theme
  • MCP server — open-sheet dev --mcp, sharing one implementation with the browser
  • Export — xlsx with live formulas, native charts, and data bars; plus csv, html, and pdf

Verified against a real spreadsheet application

CI recalculates the exported workbook in LibreOffice and diffs it against our own
evaluator, and a second test changes one assumption and asserts the whole column
moves with LibreOffice doing the arithmetic. Charts are round-tripped to confirm
their series stay bound to ranges rather than to values.

Packages

Package
@open-sheet/core compiler, formula engine, viewer, export, CLI
@open-sheet/cli npx @open-sheet/cli init
@open-sheet/mcp MCP server

Credits

Follows open-slide by @1weiho and
open-doc by
@simonliu-ai-product. This is the third medium.