Phase 5: xlsx/xls/xlsm/csv ingestion, per-cell citations, and XlsxView component

Phase 5 makes spreadsheets first-class documents: upload, extract, cite by cell address, and export with citations baked back into ExcelJS comments on the source cells. The extractors are pure side-effect-free pipelines with their own unit tests, which makes them the most portable piece of this PR.

discoverysearch

The backend gets a new backend/src/lib/extractors/ directory with two pure modules: xlsx.ts (ExcelJS, numfmt-formatted display values, formula preservation, merged range handling) and csv.ts (a handwritten RFC 4180 parser). Both are side-effect-free. Ten new Vitest unit tests cover them. .xls files are normalized to .xlsx via libreoffice-convert before extraction - that adds an OS-level LibreOffice dependency to any Docker image.

routes/documents.ts now accepts .xlsx, .xls, .xlsm, and .csv. Spreadsheets skip the PDF conversion pipeline entirely. tools/shared/documentReading.ts gains an xlsx/csv branch that calls the extractor and flattener, and appends cell-address citation guidance to the LLM input when the file type is a spreadsheet. normalizeCitation preserves Sheet!Cell strings in the page field; expandCitationToEntries routes them correctly.

exportToExcel.ts writes per-cell ExcelJS comments containing the citation list on export - citations re-attach to their source cells in the downloaded file, which closes the loop between AI output and the original spreadsheet.

The frontend gets XlsxView.tsx (413 lines): sheet tabs, sticky column-letter header and row-number gutter, a read-only formula bar showing the cell address chip and formula/value, numfmt-formatted display, click-to-select, and citation jump with a 2.5-second yellow highlight.

freeTierGuard.ts is deleted in this PR. It was blocking real documents from reaching free-tier Gemini, defeating its own purpose in a fork that wanted Gemma-default routing. A redesign is parked in CLAUDE.md "Future capabilities" gated on the connector framework, which will clarify what counts as customer data. Gemma 4 31B is added as the new default model across models.ts and ModelToggle.tsx.

So what The extractors and `XlsxView` are self-contained enough to import without taking the whole PR. The deterministic-first extractor split - pure pipelines consumed by a route - is a clean pattern that survives fork divergence. Caveats: `libreoffice-convert` adds a runtime LibreOffice dependency; ExcelJS lands in `backend/package.json`. If your fork's threat model actually depended on `freeTierGuard`, removing it is a regression - import the extractor work without the guard deletion. The Gemma 4 31B default routing change is also a configuration opinion you may not want to inherit.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

1 commit from Archibald312/GordonOSS, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
2ac696ce Phase 5: Excel I/O - xlsx/xls/xlsm/csv ingestion + per-cell citations + XlsxView Scott Rozen 2026-05-15 ↗ GitHub
commit body
Spreadsheets are now first-class documents in GordonOSS.

## Backend
- Born `backend/src/lib/extractors/` per CLAUDE.md deterministic-first rule:
  `xlsx.ts` (ExcelJS, numfmt-formatted values, formula preservation, merged
  ranges) and `csv.ts` (RFC 4180 handwritten parser) - both pure/side-effect-free
  with 10 new unit tests.
- `documents.ts`: accept xlsx/xls/xlsm/csv; xls→xlsx normalization via
  libreoffice-convert; spreadsheets skip PDF conversion; structure tree
  lists sheet names.
- `convert.ts`: `xlsToXlsx()` helper.
- `documentReading.ts`: xlsx/csv branch calls extractor+flattener; citation
  reminder appended with spreadsheet cell-address guidance when file_type is a
  spreadsheet.
- `chatTools.ts`: system prompt extended with spreadsheet citation form;
  `normalizeCitation` preserves `Sheet!Cell` strings in the `page` field.
- `models.ts`: Gemma 4 31B added as default (higher free-tier quota than
  Gemini Flash); `providerForModel` routes `gemma-*` through Gemini adapter.
- Removed `freeTierGuard.ts` and its test - guard was blocking real documents
  from free-tier Gemini. Data-privacy tier guard redesign deferred to CLAUDE.md
  "Future capabilities".
- Chat error routes now surface real `err.message` in dev instead of generic
  "Stream error".

## Frontend
- `XlsxView.tsx` (new): sheet tabs, sticky column-letter header + row-number
  gutter, read-only formula bar (cell address chip + formula/value), numfmt-
  formatted display, click-to-select, citation jump + 2.5s yellow highlight.
- `DocPanel.tsx`, `DocViewModal.tsx`, chat `page.tsx`: route xlsx/csv to
  XlsxView ahead of DocxView/DocView.
- `types.ts`: `CitationQuote.cellRef`; `expandCitationToEntries` routes
  Sheet!Cell strings; `formatCitationPage` shows cell ref verbatim.
- `exportToExcel.ts`: per-cell ExcelJS comments containing citation list.
- Upload `accept` extended to xlsx/xls/xlsm/csv in all five upload sites.
- `ModelToggle.tsx`: Gemma 4 31B added at top of Google group; set as default.
- `DocxView.tsx`: childNodes crash demoted to warn + inline fallback.
- `CLAUDE.md`: editable formula bar, generate_xlsx tool, data-privacy tier
  guard added as future capabilities.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-424.md from inside the repo you want the changes in.

⬇ Download capture-thread-424.md