oficina / sheets Public
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 oneUPDATEonrows.positionand 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.