Problem Statement
People want to feed spreadsheets to LLMs, but raw sheets are enormous in tokens — mostly
homogeneous filler, repeated values, and sparse layout that the model doesn't need spelled out
cell by cell. The SpreadsheetLLM paper's SheetCompressor solves this, but there's no clean,
reusable implementation a developer can drop into their own project. The author has rebuilt it
several times in different languages, and those copies have drifted: some hard-code invoice
business logic (a "Qty" column, "Total"/"Subtotal" row splitting), one had a live API key
committed in plaintext, only the Office Script version handles charts, and none expose a stable,
consistent library API. A developer who finds one of these can't simply pip install /
npm install it and compress a sheet — and if they use two of them in different languages, they
get different output for the same sheet.
Solution
A single repository, sheet-compressor, providing pluggable implementations of the paper's
SheetCompressor encoding in six languages — TypeScript/Node (reference), Python, C#, Go, VBA, and
Office Script — that all produce identical output for the same input, verified against one shared
golden-fixture corpus. Each implementation is a pure compression core (a function over an
in-memory grid) plus an optional thin adapter for that ecosystem's common spreadsheet library, so
a developer can pass either their own cell data or an .xlsx file. The library makes no LLM calls
and pulls in no provider SDKs; instead it ships prompt templates that teach an LLM how to read the
compressed output. Charts and graphs are represented portably as text descriptors in every
language. The invoice-specific logic is dropped; the leaked key never enters history.
User Stories
- As a developer with a 50,000-token spreadsheet, I want to compress it into a compact text
form, so that I can fit it into my LLM's context window affordably.
- As a developer, I want to
npm install / pip install / go get / add a NuGet package and
call one function, so that I can compress a sheet without copying source code.
- As a developer who already has cell values in memory, I want to pass a plain grid to the
compressor, so that I don't have to round-trip through a file.
- As a developer with an
.xlsx file, I want an optional adapter that reads it into the grid for
me, so that I don't have to wire up a spreadsheet parser myself.
- As a developer who doesn't want heavy dependencies, I want the core to work with zero required
third-party packages, so that it drops cleanly into a constrained project.
- As a developer, I want all three encodings (structural-anchor skeleton, inverted index, format
aggregation) returned, so that I can choose the best compression for my sheet's shape.
- As a developer, I want each encoding as both a raw string and a JSON object, so that I can pick
the form that fits my prompt or my downstream parsing.
- As a developer, I want a token-count estimate for the raw sheet and for each encoding, so that
I can see and report the compression ratio.
- As a developer targeting a specific model, I want the token count to use a real tokenizer when
my ecosystem has one (tiktoken, gpt-tokenizer, SharpToken, tiktoken-go), so that the number is
accurate rather than a guess.
- As a developer in an environment with no tokenizer (Office Script, VBA), I want a documented
heuristic fallback, so that I still get a reasonable estimate.
- As a developer, I want to inject my own token-counter function, so that I can match whatever
model/encoding I actually use.
- As a developer, I want documentation that recommends which encoding to use when, so that I
don't have to reverse-engineer the trade-offs.
- As a developer with charts in my sheet, I want each chart represented as a text descriptor
(type, anchor range, title, data ranges, series/axis names), so that the LLM knows the chart
exists, where it sits, and what it plots.
- As a developer, I want chart descriptors to appear in the compressed output in a consistent
syntax across all six languages, so that my prompts work regardless of language.
- As a developer on a rendering-capable host (Office Script, desktop Excel via VBA), I want the
option to also attach a base64 image of a chart, so that I can use a multimodal model.
- As a developer using a text-only model, I want the descriptor (not an image) by default, so
that I get useful chart information without multimodal overhead.
- As a developer integrating the LLM step, I want a "reader" prompt for each encoding that
teaches the model how to decode it, so that the model interprets the compressed text correctly.
- As a developer, I want ready-made task prompts (table/region detection, cell-value lookup,
sheet Q&A), so that I have working starting points instead of writing prompts from scratch.
- As a developer, I want a prompt snippet explaining how to read CHART(...) descriptors, so that
the model can reason about charts.
- As a maintainer, I want all six implementations verified against one language-neutral golden
corpus, so that they can't silently diverge.
- As a maintainer, I want a single command per language to run the conformance corpus and diff
against the goldens, so that CI catches drift on every change.
- As a maintainer, I want to regenerate the golden fixtures from the reference implementation in
one step, so that an intentional algorithm change propagates to all languages' tests at once.
- As a maintainer, I want anchor detection behind a swappable strategy interface, so that I can
upgrade it later (toward fuller paper fidelity) without changing the public output contract.
- As a maintainer, I want the input contract to allow optional per-cell metadata (data type now,
style flags later), so that the Phase 2 styling-aware anchor detection is a drop-in, not a
rewrite.
- As a contributor, I want a written spec (SPEC.md) describing the algorithm precisely, so that I
can port it to a new language without reading another language's source.
- As a user evaluating the project, I want a README that credits the SpreadsheetLLM paper and
states this is an independent implementation, so that provenance is clear.
- As a security-conscious user, I want assurance that no secrets are committed and that the old
reference sources are excluded from history, so that I can trust the repo.
- As a developer, I want the package named after what it is (the SheetCompressor component), so
that I'm not misled into thinking it's Microsoft's official release.
- As a developer processing a multi-sheet workbook, I want to compress each sheet individually,
so that I can assemble results however my application needs.
- As a developer, I want consistent A1-style cell addressing in the output that respects the
sheet's origin offset, so that addresses in the compressed form map back to real cells.
- As a developer, I want empty rows/columns dropped from the kept region appropriately, so that
the compressed output isn't padded with blanks.
- As a developer reading the inverted index, I want repeated values merged into minimal cell
ranges, so that the encoding is as compact as possible.
- As a developer reading the format aggregation, I want adjacent same-type cells merged into
ranges with a type label, so that large numeric blocks collapse.
Implementation Decisions
- Scope: compression core + prompt templates only. No LLM/HTTP calls, no provider SDKs, no
invoice/Qty domain logic. The drifted Python and VBA business logic (RemoveZeroQtyRows,
SplitByTotal/SplitSheet) is intentionally removed.
- Reference behavior is the paper pipeline as embodied in the existing Office Script
SheetCompressor.osts (structural-anchor extraction → inverted index → format aggregation).
- Six implementations: TypeScript/Node, Python, C#, Go, VBA, Office Script.
- Architecture: pure core + optional adapters. The compression core is a pure function over an
in-memory grid and has zero required dependencies. Each package additionally ships a thin,
optional adapter for its ecosystem's common spreadsheet library (SheetJS, openpyxl,
ClosedXML/EPPlus, excelize, Excel COM, ExcelScript) that builds the grid + chart descriptors from
an .xlsx.
- Input contract: rows of cell text + the sheet's origin offset (top-left row/col) + OPTIONAL
per-cell metadata (data type now; reserved for style flags later) + an OPTIONAL list of
ChartDescriptors.
- ChartDescriptor: a portable structure carrying
name, type (bar/line/pie/…), anchorRange
(A1), title, dataRanges, and series/axis names. Rendered into the encodings as a compact
text token, e.g. CHART(bar)@B5:F20 title="Sales" data=A1:D10 series=[Q1,Q2]. Base64 image
rendering is an optional, host-only extra (Office Script; VBA via COM) and is never required.
- Output contract (identical across languages): a result object exposing all three encodings —
anchor skeleton, inverted index, format aggregation — each as a raw string and a JSON form, plus
a token-estimate per stage and a raw-sheet baseline.
- Anchor detection is a swappable strategy behind a stable interface. Phase 1 strategy uses
grid-only cues: value heterogeneity (unique ÷ non-empty per row/col) combined with data-type
transitions between adjacent rows/columns, plus a k-neighborhood keep window around anchors.
Phase 2 (roadmap, out of scope for the first release) adds styling-aware detection
(borders/bold/merges/number-formats) fed through the optional per-cell metadata.
- Token counting is performed by an injectable counter. Each package's default wiring uses a
real tokenizer when the ecosystem has one (tiktoken / gpt-tokenizer or js-tiktoken / SharpToken /
tiktoken-go), declared as an optional/peer dependency so it is accurate but not forced; Office
Script and VBA fall back to one shared heuristic defined in the spec. Default encoding is
o200k_base (GPT-4o family), configurable.
- Prompt templates ship as plain strings/constants: a per-encoding "reader" explainer (anchor /
inverted-index / format-aggregation), a small set of task templates (table/region detection,
cell-value lookup, sheet Q&A), and a snippet for interpreting CHART(...) descriptors. Authored
once in a shared prompts/ source and mirrored into each package so they stay consistent.
- Per-sheet scope. The core compresses one sheet; workbook handling is "iterate sheets," not a
merged mega-encoding.
- Repository layout (monorepo):
spec/ (SPEC.md), fixtures/ (golden corpus), prompts/
(shared prompt source), packages/<language>/, docs/agents/ (skills config). MIT licensed;
README credits the paper. The local reference sources stay gitignored and out of history.
- Build order: write SPEC.md + the TypeScript reference core (which generates the golden
corpus), then port Python → C# → Go → VBA → Office Script against the goldens.
Testing Decisions
- What makes a good test here: assert external behavior — the encodings, token estimates, and
chart descriptors a given input produces — never internal helpers or intermediate data
structures. Tests are driven by inputs and expected outputs, not by inspecting how the
compressor reaches them.
- Seam 1 —
compress() core (primary). The pure compress(grid, options) → result function is
the highest, most language-neutral seam. The shared golden-fixture corpus (language-neutral input
grids + chart descriptors, with expected compressed outputs) drives it directly in every
language; each language's conformance test diffs its output against the goldens. This is where the
bulk of testing lives and is what guarantees cross-language consistency.
- Seam 2 — adapter
readSheet(file) → { grid, origin, cellMeta?, charts[] }. Host-specific, so
tested per language with a few tiny sample .xlsx files, asserting only the produced grid + chart
descriptors — NOT the compression. This isolates host coupling from the core tests.
- Seam 3 — injectable token counter. Assert the shared heuristic is deterministic and that an
injected real tokenizer is actually used in place of the heuristic.
- Deliberately not a seam: the anchor-detection strategy is exercised through
compress() by
selecting a strategy in options, not tested directly — so the Phase 2 upgrade is validated by the
same golden corpus rather than brittle internal tests.
- Prior art: the existing Office Script
SheetCompressor.osts already computes per-stage token
counts and emits the three encodings; its behavior is the seed for the reference implementation
and the first golden fixtures.
- CI: each language has a single command to run the corpus; CI runs all language jobs on every
change. Regenerating goldens from the reference implementation is a one-step operation.
Out of Scope
- Any LLM/HTTP integration, provider SDKs, API-key handling, or a chatbot/query UI.
- The invoice/Qty/Total domain logic from the old Python and VBA versions.
- Phase 2 styling-aware anchor detection (borders/bold/merges/number-formats) — designed-for via the
optional metadata contract, but not implemented in the first release.
- Server-side chart image rendering in non-host languages (Python/Go/C#/TS) — descriptors only there.
- Multi-sheet "mega-encoding" that merges sheets into one representation.
- Publishing the packages to npm/PyPI/NuGet/etc. (the libraries are built and tested in-repo first).
Further Notes
- The reference SpreadsheetLLM paper PDF and the prior implementations live locally under
sources/ and are gitignored; they are reference material, not shipped code.
- A live OpenAI API key was previously hardcoded in the old
sources/python/SpreadsheetLLM.py; it
must be revoked/rotated regardless of repo changes, and the gitignore ensures it never enters
history.
- This issue is the overarching PRD; it is expected to be broken into tracer-bullet vertical slices
(e.g. via the to-issues skill) — starting with SPEC.md + the TypeScript core + initial golden
fixtures — before implementation.
Problem Statement
People want to feed spreadsheets to LLMs, but raw sheets are enormous in tokens — mostly
homogeneous filler, repeated values, and sparse layout that the model doesn't need spelled out
cell by cell. The SpreadsheetLLM paper's SheetCompressor solves this, but there's no clean,
reusable implementation a developer can drop into their own project. The author has rebuilt it
several times in different languages, and those copies have drifted: some hard-code invoice
business logic (a "Qty" column, "Total"/"Subtotal" row splitting), one had a live API key
committed in plaintext, only the Office Script version handles charts, and none expose a stable,
consistent library API. A developer who finds one of these can't simply
pip install/npm installit and compress a sheet — and if they use two of them in different languages, theyget different output for the same sheet.
Solution
A single repository, sheet-compressor, providing pluggable implementations of the paper's
SheetCompressor encoding in six languages — TypeScript/Node (reference), Python, C#, Go, VBA, and
Office Script — that all produce identical output for the same input, verified against one shared
golden-fixture corpus. Each implementation is a pure compression core (a function over an
in-memory grid) plus an optional thin adapter for that ecosystem's common spreadsheet library, so
a developer can pass either their own cell data or an
.xlsxfile. The library makes no LLM callsand pulls in no provider SDKs; instead it ships prompt templates that teach an LLM how to read the
compressed output. Charts and graphs are represented portably as text descriptors in every
language. The invoice-specific logic is dropped; the leaked key never enters history.
User Stories
form, so that I can fit it into my LLM's context window affordably.
npm install/pip install/go get/ add a NuGet package andcall one function, so that I can compress a sheet without copying source code.
compressor, so that I don't have to round-trip through a file.
.xlsxfile, I want an optional adapter that reads it into the grid forme, so that I don't have to wire up a spreadsheet parser myself.
third-party packages, so that it drops cleanly into a constrained project.
aggregation) returned, so that I can choose the best compression for my sheet's shape.
the form that fits my prompt or my downstream parsing.
I can see and report the compression ratio.
my ecosystem has one (tiktoken, gpt-tokenizer, SharpToken, tiktoken-go), so that the number is
accurate rather than a guess.
heuristic fallback, so that I still get a reasonable estimate.
model/encoding I actually use.
don't have to reverse-engineer the trade-offs.
(type, anchor range, title, data ranges, series/axis names), so that the LLM knows the chart
exists, where it sits, and what it plots.
syntax across all six languages, so that my prompts work regardless of language.
option to also attach a base64 image of a chart, so that I can use a multimodal model.
that I get useful chart information without multimodal overhead.
teaches the model how to decode it, so that the model interprets the compressed text correctly.
sheet Q&A), so that I have working starting points instead of writing prompts from scratch.
the model can reason about charts.
corpus, so that they can't silently diverge.
against the goldens, so that CI catches drift on every change.
one step, so that an intentional algorithm change propagates to all languages' tests at once.
upgrade it later (toward fuller paper fidelity) without changing the public output contract.
style flags later), so that the Phase 2 styling-aware anchor detection is a drop-in, not a
rewrite.
can port it to a new language without reading another language's source.
states this is an independent implementation, so that provenance is clear.
reference sources are excluded from history, so that I can trust the repo.
that I'm not misled into thinking it's Microsoft's official release.
so that I can assemble results however my application needs.
sheet's origin offset, so that addresses in the compressed form map back to real cells.
the compressed output isn't padded with blanks.
ranges, so that the encoding is as compact as possible.
ranges with a type label, so that large numeric blocks collapse.
Implementation Decisions
invoice/Qty domain logic. The drifted Python and VBA business logic (
RemoveZeroQtyRows,SplitByTotal/SplitSheet) is intentionally removed.SheetCompressor.osts(structural-anchor extraction → inverted index → format aggregation).in-memory grid and has zero required dependencies. Each package additionally ships a thin,
optional adapter for its ecosystem's common spreadsheet library (SheetJS, openpyxl,
ClosedXML/EPPlus, excelize, Excel COM, ExcelScript) that builds the grid + chart descriptors from
an
.xlsx.per-cell metadata (data type now; reserved for style flags later) + an OPTIONAL list of
ChartDescriptors.
name,type(bar/line/pie/…),anchorRange(A1),
title,dataRanges, andseries/axisnames. Rendered into the encodings as a compacttext token, e.g.
CHART(bar)@B5:F20 title="Sales" data=A1:D10 series=[Q1,Q2]. Base64 imagerendering is an optional, host-only extra (Office Script; VBA via COM) and is never required.
anchor skeleton, inverted index, format aggregation — each as a raw string and a JSON form, plus
a token-estimate per stage and a raw-sheet baseline.
grid-only cues: value heterogeneity (unique ÷ non-empty per row/col) combined with data-type
transitions between adjacent rows/columns, plus a k-neighborhood keep window around anchors.
Phase 2 (roadmap, out of scope for the first release) adds styling-aware detection
(borders/bold/merges/number-formats) fed through the optional per-cell metadata.
real tokenizer when the ecosystem has one (tiktoken / gpt-tokenizer or js-tiktoken / SharpToken /
tiktoken-go), declared as an optional/peer dependency so it is accurate but not forced; Office
Script and VBA fall back to one shared heuristic defined in the spec. Default encoding is
o200k_base(GPT-4o family), configurable.inverted-index / format-aggregation), a small set of task templates (table/region detection,
cell-value lookup, sheet Q&A), and a snippet for interpreting
CHART(...)descriptors. Authoredonce in a shared
prompts/source and mirrored into each package so they stay consistent.merged mega-encoding.
spec/(SPEC.md),fixtures/(golden corpus),prompts/(shared prompt source),
packages/<language>/,docs/agents/(skills config). MIT licensed;README credits the paper. The local reference sources stay gitignored and out of history.
corpus), then port Python → C# → Go → VBA → Office Script against the goldens.
Testing Decisions
chart descriptors a given input produces — never internal helpers or intermediate data
structures. Tests are driven by inputs and expected outputs, not by inspecting how the
compressor reaches them.
compress()core (primary). The purecompress(grid, options) → resultfunction isthe highest, most language-neutral seam. The shared golden-fixture corpus (language-neutral input
grids + chart descriptors, with expected compressed outputs) drives it directly in every
language; each language's conformance test diffs its output against the goldens. This is where the
bulk of testing lives and is what guarantees cross-language consistency.
readSheet(file) → { grid, origin, cellMeta?, charts[] }. Host-specific, sotested per language with a few tiny sample
.xlsxfiles, asserting only the produced grid + chartdescriptors — NOT the compression. This isolates host coupling from the core tests.
injected real tokenizer is actually used in place of the heuristic.
compress()byselecting a strategy in options, not tested directly — so the Phase 2 upgrade is validated by the
same golden corpus rather than brittle internal tests.
SheetCompressor.ostsalready computes per-stage tokencounts and emits the three encodings; its behavior is the seed for the reference implementation
and the first golden fixtures.
change. Regenerating goldens from the reference implementation is a one-step operation.
Out of Scope
optional metadata contract, but not implemented in the first release.
Further Notes
sources/and are gitignored; they are reference material, not shipped code.sources/python/SpreadsheetLLM.py; itmust be revoked/rotated regardless of repo changes, and the gitignore ensures it never enters
history.
(e.g. via the
to-issuesskill) — starting with SPEC.md + the TypeScript core + initial goldenfixtures — before implementation.