oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1# Brainstorm — Fable's answer
2
3Read in the brief's order, then `handlers_grid.go`, `viewspec.go`,
4`grid.go`, `store/handle*.go`, `store/history.go`, `calc/registry.go`,
5`calc/graph.go`, `calc/translate.go`, the four chrome modules, and the
6`sheet.html`, `table.html` and `comments.html` templates. I did not open
7`BRAINSTORM-GPT.md`; the brief wants two independent voices.
8
9## 1. Insert and delete a row or a column
10
11**What it is.** Right-click a row, or open Edit, and put a new empty row
12above it; every formula still points at the numbers it pointed at
13before. Delete works the same way, and a formula that referred to the
14deleted row shows `#REF!` rather than a wrong number.
15
16**Why it is first.** It is the most basic act in a spreadsheet after
17typing, and the app cannot do it: `grid.js` sets `allowInsertRow:
18false`, no route touches `Row.Position`, and `handle_open.go`'s binder
19comment says exactly why. What people do instead is retype a block one
20row down — which writes an input event per moved cell, detaches every
21comment (`comments.row_id`) from the data it was about, and leaves
22history describing a hundred edits nobody made on purpose. PRODUCT.md's
23job is "put numbers in a shared grid"; a grid you cannot make room in is
24a form.
25
26**What it costs.** This is §14's subsystem wearing a feature's name, and
27I am saying so: calc's key space rebuilt on stable ids. Today
28`calc.Ref` carries positions, `refKey` and `rangeCells` compute
29`CellKey{pos+1}` deep inside Eval, `rawText` and `cells.input_sealed`
30hold A1 text, and `events.payload_sealed` holds A1 text. Moving a row
31therefore needs one of two designs, and **Paul has to rule which**:
32
33- *(a)* rewrite the A1 text of every affected formula at insert time —
34 Excel's mechanism. Cheap to start, but either the log grows by one
35 event per rewritten formula or it does not and a restored old formula
36 points at the wrong row.
37- *(b)* bind references to `(row_id, col_id)` at write time, persist the
38 id-bound form, render A1 from the position map. This is what §4.2
39 already claims the design does. Insert becomes one `UPDATE` on
40 `rows.position` and no graph change. Events written before the change
41 stay A1 and are "coarse before the horizon", which §4.3's retention
42 language already permits.
43
44I would argue for (b). Touches `calc/ref.go`, `eval.go`, `fn_lookup.go`,
45`graph.go`; `store/handle*.go`, `events.go`, `history.go`;
46`xlsx/export.go` (renders A1); `grid.js`, the Edit and Insert menus, and
47`table.html` for the scriptless twin. No SQL migration is forced; one
48additive column marking payload encoding is likely. It is the largest
49item here by a factor of three and needs a §1 ruling plus a §Review pass
50on the spec change before code.
51
52**What breaks.** A wrong binding silently repoints every formula — the
53exact failure §14 names — and nothing on screen says so. The one thing
54that would make me drop it: if the dogfood sheets turn out to be
55append-only (rows added at the bottom, never between), `EnsureRow`
56already covers that and the subsystem can wait for a second ask.
57
58**First slice.** Insert one row above / one column left, on the
59id-bound key space, from the Edit menu and from a form on the table
60view. Delete and `#REF!` second. No destructive sort at all (see below).
61
62## 2. The functions people's existing spreadsheets already use
63
64**What it is.** `=SUMIF(A:A,">100",B:B)`, `=COUNTIF`, `=YEAR(A2)`,
65`=EOMONTH`, `=ISBLANK`, `=TEXT` — the everyday tail. A workbook you
66import stops showing `#NAME?` where its author had totals.
67
68**Why it is second.** The engine registers 32 functions
69(`calc/registry.go` and `fn_*.go`: SUM through VLOOKUP, NOW, TODAY);
70spec §5 said "~120 to start"; the LibreOffice corpus has 60 entries over
7130 of them. An unknown name evaluates to `#NAME?` (`eval_call.go`), so
72`xlsx.Import` faithfully turns a real budget into a sheet of errors and
73the person blames the app, not the gap. This is the cheapest item on the
74list and has the best test story in the repo: `tools/gencorpus` produces
75vectors from LibreOffice headless (gate 4), so every function ships
76proven against a real spreadsheet rather than our reading of a spec.
77
78**What it costs.** `calc` only. New family files, self-registering; a
79shared criteria matcher for `">100"` and `"*x*"`; date functions ride
80the serial model `format/dates.go` already displays. Corpus regen. No
81migration, no UI, no ruling, no chrome. A few sessions.
82
83**What breaks.** A function that disagrees with LibreOffice on a corner
84case — the corpus catches it if the case is in the corpus, which is the
85discipline `volatile-excluded.txt` already models. I would drop it only
86if the event log showed the dogfood sheets were literal-only, and it
87does not.
88
89**First slice.** SUMIF, COUNTIF, AVERAGEIF, SUMIFS, COUNTIFS; DATE,
90YEAR, MONTH, DAY, DAYS, EOMONTH, WEEKDAY; ISBLANK, ISNUMBER, ISTEXT,
91ISERROR; TEXT, VALUE. About twenty.
92
93## 3. A second tab
94
95**What it is.** A `+` on the tab strip, a name you can change, and a
96strip you can switch with. Importing a three-worksheet file gives you
97three tabs, not one.
98
99**Why it is third.** It fires a written trigger — §14: "a second tab is
100reachable in the UI" — and the tab strip in `sheet.html` says in a
101comment that it deliberately has no `+` for exactly that reason. The
102`tabs` table (sealed name, position) exists; the API routes already
103carry `{tabID}`; `xlsx.Import` reads `GetSheetList()[0]` and stops. Real
104workbooks have tabs, and the import form's hint is an honest apology
105that should retire.
106
107**What it costs.** Medium, no migration, no ruling. The page routes
108(`/table`, `/history`, `/comments`, `/view`) use `SheetSummary.TabID` =
109first tab (`sheetsvc.go`); they need a tab parameter. Create and rename
110are forms, so the scriptless path is free. Import loops over
111worksheets. **Cross-tab references (`Sheet2!A1`) are not in this** and I
112would refuse to build them before pick 1: real `Row.ID`/`Col.ID` are
113globally unique, so an id-bound key space is already tab-agnostic and
114cross-tab refs become "one handle per sheet". Built on the position key
115space they would be a second binding to unpick later.
116
117**What breaks.** A tab you can create in the grid but not from the
118table view — principle 2. Drop it if none of the dogfooders' imported
119files has a second worksheet.
120
121**First slice.** `+`, rename, switch, and import of every worksheet.
122No reorder, no delete, no cross-tab refs.
123
124## Quietly unfinished: the grid shows A1:Z100 and says nothing
125
126`sheetsvc.go` sets `defaultWindowRows = 100`, `defaultWindowCols = 26`;
127`handlers_sheets.go:142` hands exactly those to the page; `grid.js`
128fetches exactly those; `snapshot` (`grid.go`) drops every cell outside
129them. Meanwhile `handleCellPost` accepts row 4,999, the table paginates
130to 500, import caps at 200,000 cells, the thumbnail (`preview.go`) draws
131the sheet's *true* shape, and export carries everything. Plan 2 item 5
132promised the window would be "extendable by an explicit button"; there
133is no such button in `grid.js` or `sheet.html`. So an imported 300-row
134sheet opens showing 100 rows, with no line of copy anywhere saying rows
135101–300 exist. That is principle 1 broken by omission, and it matters
136more than tabs because it makes the shipped product wrong rather than
137merely smaller. Smallest fix: the page passes the tab's extent (from
138`GridRows()`, plus a buffer, clamped at `maxWindowRows`), the grid sizes
139to it, and a status line says "rows 1–500 of 1,240" with a "Show more"
140button as the menu equivalent. About sixty lines. Virtualise later.
141
142## Would not build: destructive sort
143
144§14 defers it behind the key-space subsystem. I would not build it even
145after pick 1 lands, and I think the trigger is wrong. §4.2 rules "values
146move, ids stay with positions", so a sort is a cell-content rewrite:
147500 rows by 10 columns is 5,000 sealed input events per sort in a log
148Litestream ships continuously; undo is own-edits, cell-level (§7), so
149undoing a sort is 5,000 undos or a new compound event kind; and
150comments anchor to `row_id`, so the note about Acme's row stays at
151position 7 while Acme's numbers move to 30 — the anchoring that stable
152ids were bought for, defeated by the sort semantics in the same
153section. The non-destructive view (`viewspec.go`) already gives people
154what they want and tells them the truth about it. The Google pull is
155"sort should stick"; it does. If Paul wants it anyway, the trigger
156should read "a compound event kind with its own undo", not "stable ids".
157
158## What I would cut from a longer list
159
160Comments visible in the grid (a per-cell count is shape, not content;
161worth an afternoon, not a slot), presence, freeze panes, find, undo for
162formatting, conditional formatting, charts. All real; none moves the
163dogfood job.
164