oficina / sheets Public

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
Paul Campbell pushed by paul@keymail.dev 964a8bf7e9664d56de1e184a7d6db12aac3cac6d parent 7c8ad66
2 files changed, +44 −3
  • PRODUCT.md +12 −3
  • docs/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 | — |