# Brainstorm — Fable's answer

Read in the brief's order, then `handlers_grid.go`, `viewspec.go`,
`grid.go`, `store/handle*.go`, `store/history.go`, `calc/registry.go`,
`calc/graph.go`, `calc/translate.go`, the four chrome modules, and the
`sheet.html`, `table.html` and `comments.html` templates. I did not open
`BRAINSTORM-GPT.md`; the brief wants two independent voices.

## 1. Insert and delete a row or a column

**What it is.** Right-click a row, or open Edit, and put a new empty row
above it; every formula still points at the numbers it pointed at
before. Delete works the same way, and a formula that referred to the
deleted row shows `#REF!` rather than a wrong number.

**Why it is first.** It is the most basic act in a spreadsheet after
typing, and the app cannot do it: `grid.js` sets `allowInsertRow:
false`, no route touches `Row.Position`, and `handle_open.go`'s binder
comment says exactly why. What people do instead is retype a block one
row down — which writes an input event per moved cell, detaches every
comment (`comments.row_id`) from the data it was about, and leaves
history describing a hundred edits nobody made on purpose. PRODUCT.md's
job is "put numbers in a shared grid"; a grid you cannot make room in is
a form.

**What it costs.** This is §14's subsystem wearing a feature's name, and
I am saying so: calc's key space rebuilt on stable ids. Today
`calc.Ref` carries positions, `refKey` and `rangeCells` compute
`CellKey{pos+1}` deep inside Eval, `rawText` and `cells.input_sealed`
hold A1 text, and `events.payload_sealed` holds A1 text. Moving a row
therefore needs one of two designs, and **Paul has to rule which**:

- *(a)* rewrite the A1 text of every affected formula at insert time —
  Excel's mechanism. Cheap to start, but either the log grows by one
  event per rewritten formula or it does not and a restored old formula
  points at the wrong row.
- *(b)* bind references to `(row_id, col_id)` at write time, persist the
  id-bound form, render A1 from the position map. This is what §4.2
  already claims the design does. Insert becomes one `UPDATE` on
  `rows.position` and no graph change. Events written before the change
  stay A1 and are "coarse before the horizon", which §4.3's retention
  language already permits.

I would argue for (b). Touches `calc/ref.go`, `eval.go`, `fn_lookup.go`,
`graph.go`; `store/handle*.go`, `events.go`, `history.go`;
`xlsx/export.go` (renders A1); `grid.js`, the Edit and Insert menus, and
`table.html` for the scriptless twin. No SQL migration is forced; one
additive column marking payload encoding is likely. It is the largest
item here by a factor of three and needs a §1 ruling plus a §Review pass
on the spec change before code.

**What breaks.** A wrong binding silently repoints every formula — the
exact failure §14 names — and nothing on screen says so. The one thing
that would make me drop it: if the dogfood sheets turn out to be
append-only (rows added at the bottom, never between), `EnsureRow`
already covers that and the subsystem can wait for a second ask.

**First slice.** Insert one row above / one column left, on the
id-bound key space, from the Edit menu and from a form on the table
view. Delete and `#REF!` second. No destructive sort at all (see below).

## 2. The functions people's existing spreadsheets already use

**What it is.** `=SUMIF(A:A,">100",B:B)`, `=COUNTIF`, `=YEAR(A2)`,
`=EOMONTH`, `=ISBLANK`, `=TEXT` — the everyday tail. A workbook you
import stops showing `#NAME?` where its author had totals.

**Why it is second.** The engine registers 32 functions
(`calc/registry.go` and `fn_*.go`: SUM through VLOOKUP, NOW, TODAY);
spec §5 said "~120 to start"; the LibreOffice corpus has 60 entries over
30 of them. An unknown name evaluates to `#NAME?` (`eval_call.go`), so
`xlsx.Import` faithfully turns a real budget into a sheet of errors and
the person blames the app, not the gap. This is the cheapest item on the
list and has the best test story in the repo: `tools/gencorpus` produces
vectors from LibreOffice headless (gate 4), so every function ships
proven against a real spreadsheet rather than our reading of a spec.

**What it costs.** `calc` only. New family files, self-registering; a
shared criteria matcher for `">100"` and `"*x*"`; date functions ride
the serial model `format/dates.go` already displays. Corpus regen. No
migration, no UI, no ruling, no chrome. A few sessions.

**What breaks.** A function that disagrees with LibreOffice on a corner
case — the corpus catches it if the case is in the corpus, which is the
discipline `volatile-excluded.txt` already models. I would drop it only
if the event log showed the dogfood sheets were literal-only, and it
does not.

**First slice.** SUMIF, COUNTIF, AVERAGEIF, SUMIFS, COUNTIFS; DATE,
YEAR, MONTH, DAY, DAYS, EOMONTH, WEEKDAY; ISBLANK, ISNUMBER, ISTEXT,
ISERROR; TEXT, VALUE. About twenty.

## 3. A second tab

**What it is.** A `+` on the tab strip, a name you can change, and a
strip you can switch with. Importing a three-worksheet file gives you
three tabs, not one.

**Why it is third.** It fires a written trigger — §14: "a second tab is
reachable in the UI" — and the tab strip in `sheet.html` says in a
comment that it deliberately has no `+` for exactly that reason. The
`tabs` table (sealed name, position) exists; the API routes already
carry `{tabID}`; `xlsx.Import` reads `GetSheetList()[0]` and stops. Real
workbooks have tabs, and the import form's hint is an honest apology
that should retire.

**What it costs.** Medium, no migration, no ruling. The page routes
(`/table`, `/history`, `/comments`, `/view`) use `SheetSummary.TabID` =
first tab (`sheetsvc.go`); they need a tab parameter. Create and rename
are forms, so the scriptless path is free. Import loops over
worksheets. **Cross-tab references (`Sheet2!A1`) are not in this** and I
would refuse to build them before pick 1: real `Row.ID`/`Col.ID` are
globally unique, so an id-bound key space is already tab-agnostic and
cross-tab refs become "one handle per sheet". Built on the position key
space they would be a second binding to unpick later.

**What breaks.** A tab you can create in the grid but not from the
table view — principle 2. Drop it if none of the dogfooders' imported
files has a second worksheet.

**First slice.** `+`, rename, switch, and import of every worksheet.
No reorder, no delete, no cross-tab refs.

## Quietly unfinished: the grid shows A1:Z100 and says nothing

`sheetsvc.go` sets `defaultWindowRows = 100`, `defaultWindowCols = 26`;
`handlers_sheets.go:142` hands exactly those to the page; `grid.js`
fetches exactly those; `snapshot` (`grid.go`) drops every cell outside
them. Meanwhile `handleCellPost` accepts row 4,999, the table paginates
to 500, import caps at 200,000 cells, the thumbnail (`preview.go`) draws
the sheet's *true* shape, and export carries everything. Plan 2 item 5
promised the window would be "extendable by an explicit button"; there
is no such button in `grid.js` or `sheet.html`. So an imported 300-row
sheet opens showing 100 rows, with no line of copy anywhere saying rows
101–300 exist. That is principle 1 broken by omission, and it matters
more than tabs because it makes the shipped product wrong rather than
merely smaller. Smallest fix: the page passes the tab's extent (from
`GridRows()`, plus a buffer, clamped at `maxWindowRows`), the grid sizes
to it, and a status line says "rows 1–500 of 1,240" with a "Show more"
button as the menu equivalent. About sixty lines. Virtualise later.

## Would not build: destructive sort

§14 defers it behind the key-space subsystem. I would not build it even
after pick 1 lands, and I think the trigger is wrong. §4.2 rules "values
move, ids stay with positions", so a sort is a cell-content rewrite:
500 rows by 10 columns is 5,000 sealed input events per sort in a log
Litestream ships continuously; undo is own-edits, cell-level (§7), so
undoing a sort is 5,000 undos or a new compound event kind; and
comments anchor to `row_id`, so the note about Acme's row stays at
position 7 while Acme's numbers move to 30 — the anchoring that stable
ids were bought for, defeated by the sort semantics in the same
section. The non-destructive view (`viewspec.go`) already gives people
what they want and tells them the truth about it. The Google pull is
"sort should stick"; it does. If Paul wants it anyway, the trigger
should read "a compound event kind with its own undo", not "stable ids".

## What I would cut from a longer list

Comments visible in the grid (a per-cell count is shape, not content;
worth an afternoon, not a slot), presence, freeze panes, find, undo for
formatting, conditional formatting, charts. All real; none moves the
dogfood job.
