docs: say which of import and export is actually reachable
PRODUCT.md listed "XLSX/CSV import and export" among the things built and working. That was true of the converter and false of the product: import had no route until this branch, xlsx.Export still has none, nothing downloads a file, and there is no CSV code in the repo at all. A capability list that names a package rather than a reachable affordance is how one stops describing the thing it names — it is what sent this task looking for a bug in a feature that had never been wired up. Spec §11 gains a dated note recording what landed and the three decisions it needed, matching §11.1's shape. §14 gains the first-worksheet limit with a written trigger: a second tab being reachable in the UI. Importing into tabs today would write rows no screen can open, which is worse than importing one worksheet honestly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H6YuJ2QG6koRKjNf3ddr1Q
2 files changed,
+44
−3
PRODUCT.md+12 −3docs/superpowers/specs/2026-08-30-sheets-design.md+32 −0
CI passed — run details
diff --git a/PRODUCT.md b/PRODUCT.md| index 348d560..b9a8e00 100644 |
| --- a/PRODUCT.md |
| +++ b/PRODUCT.md |
| @@ -70,10 +70,19 @@ confirmed binding by Paul on 2026-09-01: |
| ## Capabilities and Constraints |
| -**Built and working as of 2026-09-01:** sign-in, sheet list and creation, |
| +**Built and working as of 2026-09-02:** sign-in, sheet list and creation, |
| a grid backed by a Go calc engine, live sync across tabs over SSE, a |
| -scriptless paginated table view with per-cell editing, XLSX/CSV import |
| -and export, and a contrast notice for imported fills. |
| +scriptless paginated table view with per-cell editing, XLSX import |
| +(upload or drag a file onto the list; it runs as a background job and |
| +lands on the new sheet), and a contrast notice for imported fills. |
| + |
| +**Not reachable yet, though the code is there.** This line previously |
| +read "XLSX/CSV import and export", which was true of the converter and |
| +false of the product: `xlsx.Export` has no route and nothing downloads a |
| +file, and there is no CSV code in the repo at all. Import was in the |
| +same state until 2026-09-02 — a tested converter with no way to reach |
| +it — which is exactly how a capability list stops describing the thing |
| +it names. Export's HTTP surface and CSV are both still to build. |
| **Constraints that bind design:** |
diff --git a/docs/superpowers/specs/2026-08-30-sheets-design.md b/docs/superpowers/specs/2026-08-30-sheets-design.md| index df97a7c..7aaa777 100644 |
| --- a/docs/superpowers/specs/2026-08-30-sheets-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-sheets-design.md |
| @@ -861,6 +861,37 @@ error text carries coordinates and codes, never content (§2.4).** |
| Uploaded and generated files go to `blobs.Sealed` with |
| `ContentType: application/octet-stream` (§2.3). |
| +**Import's HTTP surface built 2026-09-02.** The converter and its |
| +round-trip corpus predated this by two days; what did not exist was any |
| +way to reach them — no route, no upload, no job, and a `PRODUCT.md` that |
| +listed import among the things "built and working". `POST /sheets/import` |
| +now takes a workbook, makes the sheet, and runs `xlsx.Import` as a |
| +`jobs` job that 303s to `/jobs/{id}` and lands on the finished sheet. |
| +Three things the design had not had to decide, decided here: |
| + |
| +- **The sheet is created before the job, and removed if the job fails.** |
| + `jobs.Start` needs the finished location up front and that location |
| + must be server-built, so the sheet id has to exist first. The cost is |
| + a sheet that briefly exists without contents; a failed import |
| + soft-deletes it, because a half-imported sheet looks complete, sits in |
| + a list the whole team can see, and its only contradiction is a status |
| + page nobody else opened. |
| +- **The upload is not retained at all.** The rule above governs keeping |
| + one; not keeping it satisfies the same concern more simply, and no |
| + plaintext workbook reaches storage in any form. `xlsx.StoreSealed` is |
| + still there for the task that wants the original back. |
| +- **Job error text is replaced, never wrapped.** rastrillo's runner |
| + slogs the error a job returns, and `carlos logs` is platform-held. So |
| + `importFailure` builds fresh text from a coordinate and a code, and |
| + `xlsx.CellError` carries that coordinate as data — its `Error()` omits |
| + its own cause by construction, so printing one cannot leak a cell even |
| + by accident. |
| + |
| +**Drag and drop is the form.** The list's drop zone fills the same file |
| +input and submits the same form, so there is one route and one code |
| +path, and §6's scriptless floor holds without a second implementation to |
| +keep in step with the first. |
| + |
| ### 11.1 A retained fill can be unreadable, and that is correct |
| Retaining an imported fill's original hex (§10.3) means we also carry the |
| @@ -1010,6 +1041,7 @@ it is a fallback rather than a scramble. |
| | Pegamento's real vault facet (sealed team names, §9.2) | The vault facet ships with a recovery story | |
| | Moving the provisioning queue out of the home (§9.4) | The home spec rules on it | |
| | **Destructive sort — physically reordering rows** | calc's dependency graph is keyed on POSITION (`store/handle.go`'s `cellKey` builds `CellKey{rowPos+1, colPos+1}`), so moving a row changes what every existing formula points at, silently. Plan 1's review flagged this as "valid only until rows move". Trigger: **calc's key space rebuilt on stable ids**, which is a subsystem and not a feature. Sort and filter ship non-destructively meanwhile — they reorder what is displayed and never what is stored, which is correct rather than a stopgap. | |
| +| **Importing a workbook's second and later worksheets** | `xlsx.Import` reads `GetSheetList()[0]` and stops, so a three-worksheet file becomes a one-tab sheet. Stated in the import form's own hint rather than discovered afterwards (§11), because a workbook that arrives looking whole is the quiet kind of wrong PRODUCT.md's first principle forbids. Trigger: **a second tab is reachable in the UI**. Sheets already has a `Tab` table and `SheetSummary.TabID` already means "the first tab", but nothing creates, names, lists or switches tabs — so importing into tabs today would write rows no screen can open, which is worse than importing one worksheet honestly. | |
| | Pivot tables | Asked for twice | |
| | Rich presence beyond who's-here | Teams above ~5 concurrent editors | |
| | Offline editing | — | |