oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Sheets
2
3A CARLOS spreadsheet. One instance per team.
4
5This file is binding. Where it disagrees with a general skill, this file
6wins. Where it disagrees with the design spec, the spec wins and this file
7is wrong β€” fix it in the same commit. Both files live here, so that is a
8rule this repository can actually keep.
9
10Where the design spec disagrees with the suite sharing doctrine
11(`oficina/suite`), the doctrine wins and **the spec is wrong β€” fix it by
12citing the ruling, not by restating it**. That cannot be one commit, because
13the doctrine is another repository; a cross-repository request goes through
14`amadan discuss` on the repo whose text must change.
15
16**The design:** `docs/superpowers/specs/2026-08-30-sheets-design.md`. Its
17Β§1 is the settled-decisions table: dated, attributed, with the rejected
18alternative beside each. Don't relitigate one in a drive-by.
19
20---
21
22## The one rule
23
24> **A sheet's contents are sealed at rest. The bucket never holds a
25> plaintext cell.**
26
27The instance holds the key while it runs, so the server *can* read a sheet
28it has open. That is a deliberate softening, not an accident, and the spec's
29Β§2 names every leak it produces. What the rule forbids absolutely is a
30plaintext value reaching the replicated SQLite file or object storage.
31
32A test greps raw SQLite bytes and bucket objects for plaintext from the
33first commit. `plaintextExempt` starts empty; any entry is dated, justified,
34and listed in the README.
35
36**We never claim "we can't read your data."** A compromised live host can
37read any open sheet. The README says so and the UI never implies otherwise.
38
39---
40
41## Review β€” RULED 2026-08-30 by Paul
42
43**Every spec and every branch is reviewed by the highest-capability model
44available before it is approved or merged.**
45
46Not the model that happened to write it, and not whichever model the
47session started on. Cheaper models draft; the most capable one available
48reviews. A review by a weaker model than the author is not a review.
49
50Mechanically:
51
52- **Specs** β€” a review pass before the spec is presented for human approval,
53 and again after any material change to a settled decision.
54- **Branches** β€” a review pass before the PR is opened, and again after any
55 force-push that changes more than a rebase.
56- **Record which model reviewed**, in the PR body or the spec's status line.
57 A review nobody can attribute is a review nobody can check.
58
59**The highest-capability model is discovered, not hardcoded.** At the time
60of writing that is Opus; naming a model here without saying how to re-derive
61it is how this rule goes stale silently. The check is: what is the most
62capable model this session can actually reach right now. If a session cannot
63reach one at least as capable as the author's, it says so and waits rather
64than passing a weaker review off as done.
65
66This is a floor, not a ceiling β€” an extra pair of eyes, human or model, is
67always welcome on top.
68
69---
70
71## Shipping cadence β€” RULED 2026-08-30 by Paul
72
73**Everything happens on a worktree, on a branch.** Never directly on
74`main`, and never in the primary checkout.
75
761. **Worktree per session.** One isolated workspace per piece of work.
772. **Branch per piece of work**, cut from current `main`.
783. **Commit regularly and push to the branch.** Small commits that tell the
79 story, pushed as they land β€” not one heroic commit at the end. A branch
80 that only exists locally is work nobody can see or recover.
814. **Squash-merge to `main`** when, and only when, all three hold:
82 - the work is **done** β€” not "done except",
83 - it is **approved** β€” including the Β§Review pass by the
84 highest-capability model available,
85 - **CI is green** β€” actually green, checked, not assumed.
86
87Any one of the three missing means it does not merge. "CI was green
88before the last push" is not CI being green.
89
90Review happens on a **deployed canary**, never localhost. Shared hosts only
91ever run merged `main`.
92
93**Every branch gets a discussion, and that discussion IS the review
94record** β€” including which model reviewed it (Β§Review). A discussion
95survives a squash where a PR body does not. Tasks are a checklist, not a
96log: commentary belongs in the discussion.
97
98**A refusal gets surfaced in the discussion, never routed around.** Straight
99from amadan's own house rules, and the rule most likely to be quietly
100ignored.
101
102---
103
104## Intent before code
105
106CARLOS's first principle: the design doc precedes the build, and its
107decisions are taken. If one proves wrong, **stop and flag it** rather than
108quietly redesigning around it.
109
110Keep the spec's "Deliberately not built yet" table honest. Every deferred
111thing has a written trigger. A thing deferred without a trigger is a thing
112forgotten.
113
114---
115
116## Mechanical rules
117
118Rules here are enforced by tests, not by memory. If you find yourself
119relying on remembering one, make it a test instead.
120
121**Data**
122- **Never `scope.Owned` or `scope.OwnedBy` on a team-shared table.** Both
123 are single-column equality (`Owned` is literally `Where("user_id = ?")`),
124 and our sheets, tabs and cells are team-shared. Applied to them, a sheet
125 list returns **only the creator's rows** β€” it fails as a plausible empty
126 list, not as an error, which is the worst available failure shape and also
127 the default thing to reach for. Access goes through the hand-written
128 grants join. `scope.Owned` is still correct for genuinely per-user rows.
129- A row that isn't yours **404s, never 403s** β€” preserved by hand in the
130 grants join, and test-enforced by the two-user isolation suite.
131- Scope `tx`, never the outer handle, inside transactions, or the
132 one-connection writer pool deadlocks.
133- Never bind a request body onto a GORM model. Explicit `map[string]any`
134 plus a `.Select` allowlist.
135- Resolve the viewer through the app's `identity` and `actor` helpers. Local
136 sessions retain verified addresses; opt-in Home sessions contain opaque Home
137 subjects and must resolve through the shared connection to existing members.
138 Never create members from Home subjects or infer links from matching addresses.
139 Never use `sessions.UserID`: it does not resolve either identity shape.
140- Home links require independent fresh local and Home proofs plus confirmation.
141 Recheck existing admission and sheet grants on every Home request. Member IDs
142 are AUTOINCREMENT and must never be reassigned; any future deletion path must
143 remove the member's `home_identity_links` rows in the same transaction.
144- Migrations are additive-only. New code over an old DB is always safe.
145 Never delete data to update.
146- **Cell values are IEEE doubles end to end.** Currency is a number format,
147 not a type; `=A1*1.05` produces a double and XLSX stores a double.
148 Storing cents would round the evaluator's own output. The family's "a
149 float never touches an amount" rule binds **app-owned** amounts only β€”
150 billing, quotas, anything Sheets itself counts.
151- **No cell content, comment text, sheet title or tab name in any log line,
152 error string, job status message or metric label.** `carlos logs` is
153 platform-held storage we cannot grep. Errors carry coordinates and codes.
154
155**Frontend**
156- No build step. Jspreadsheet CE is vendored as a static asset, not fetched
157 from a CDN and not bundled.
158- **Both vendored stylesheets paint in literals, and every rule the app
159 can render must be restated in `sheets.css`.** `TestVendorPaintIsRestated`
160 holds the list and checks it in both directions β€” that each vendored
161 selector still exists (v5 renamed every `.jexcel` to `.jss_*` once
162 already) and that we restate it. Rules the app cannot render are named
163 in that test with a checkable reason, never silently dropped.
164 Two more guards sit beside it: no colour literal may appear in
165 `sheets.css` at all, and `sheets.css` must stay linked before the head
166 block that carries the vendored links.
167- **Restate at `> tbody > tr > td.foo`, never at the library's own
168 `.jss_worksheet .foo`.** Two independent reasons, and the second is the
169 one that bites: our grid-line rule is one specificity step higher than
170 the library's, *and* `sheet.html` links the vendored stylesheets into a
171 head block that `layout.html` renders after `sheets.css`, so the
172 library is later in source order and an equal-specificity restatement
173 loses on order alone. Ours must be strictly higher, never merely equal.
174 Written short, a restatement parses, uses a token, and paints nothing β€”
175 which is how the grid's selection outline went missing in dark for the
176 whole of the `core` branch.
177- **Write the attribute grammar: `rst-btn="primary compact"`, not
178 `class="rst-btn rst-btn--primary"`.** This reverses the earlier rule in
179 this file, which was written against v0.20.0 where the attribute
180 spelling genuinely did not exist. It landed in v0.21.0 and is complete
181 in the pinned v0.23.0: measured against
182 `$(go env GOMODCACHE)/github.com/carlosframework/rastrillo@v0.23.0/ui/tokens.css`,
183 every class selector has an attribute twin, and rastrillo enforces that
184 itself with `TestEveryClassSelectorHasAnAttributeTwin`. Both spellings
185 are specificity `(0,1,0)`, so pairing changes no cascade. The class
186 spelling is scheduled for removal before 1.0.
187 - The grammar: a bare attribute is the KIND (`rst-callout`); a token in
188 its value is a VARIANT, matched with `~=` so values compose like a
189 class list; a PART is its own flat attribute (`rst-callout-body`),
190 never a value.
191 - **Utilities stay classes.** Seven names are class-only by design, and
192 correctly so: `rst-grow`, `rst-mono`, `rst-sr-only`, `rst-m-hide`,
193 `rst-danger`, `rst-nm`, `rst-cell-mut`. An attribute says what a thing
194 *is*; a class says what is being applied to it.
195 - `rst-tone` pairs with `data-tone` rather than with a class.
196 - Adopt per screen, never per element β€” mixed spellings inside one
197 component match neither.
198- **rastrillo is `amadan.net/rastrillo/rastrillo`.** The old path,
199 `github.com/carlosframework/rastrillo`, stops at v0.23.0: code importing
200 it gets no fix, and its own checks cannot tell (Sheets zoomed iOS on
201 every tap for a month that way). `TestNoPreMoveRastrillo` allows one
202 import of it, native.go's bridge to rastrillo/native's server, until
203 that server moves. The CLI is pinned as a `tool` in go.mod: run it as
204 `go tool rastrillo …`, never a `rastrillo` from PATH.
205- **After `go tool rastrillo doctor --fix`, delete the scripts it adds**
206 (`static/rastrillo.js`, `select.js`, `datetime.js`, `calendar.js`).
207 Sheets serves estilo's shim and busy.js from memory and uses no other
208 rastrillo script; the shim doctor adds at this pin still carries the
209 old busy rule, which fights estilo's. `TestBusyRuleIsLinkedAndServed`
210 fails if one is left behind.
211- **Run `rastrillo doctor` on every rastrillo upgrade**, and
212 `rastrillo doctor --fix` to re-copy what drifted. `static/tokens.css` is
213 frozen at scaffold time while the partials it styles keep upgrading with
214 the module, so an app can run new markup against old CSS and see nothing
215 worse than a slightly wrong screen. `doctor` shipped in v0.22.0 (verified:
216 `rastrillo doctor --help` offers `-fix`, `-force`, `-theme`). It reports a
217 version mismatch as a mismatch rather than as drift and refuses `--fix`
218 across one, since copying newer assets into an app built against an older
219 framework manufactures the fault it is looking for. **So upgrade the
220 `doctor` binary alongside the module** β€” a `doctor` one release behind
221 compares against its own library copy and calls the upgrade you just did
222 the drift.
223- **Theme bytes come from the Estilo module pin.** There is no app-local
224 `static/theme.css` to copy. On framework upgrades, update the Estilo pin
225 to the same released `day` body and run the theme asset tests. Estilo's
226 byte-for-byte test and this app's `theme_assets_test.go` detect drift;
227 `rastrillo doctor` reports the absent theme as not compared. Its token
228 checks still apply. Do not recreate an embedded theme file that the
229 served filesystem would shadow. This matters at v0.23.0 because tokens
230 require `--rst-header-rule`; an older theme silently loses the rule.
231- Don't build on `.rst-page-header::after` β€” the rake line is retired in
232 favour of `--rst-header-rule` (rastrillo v0.23.0, design doc Β§6-v2.2).
233 It was retired because, built from the same parts in the same order, it
234 was indistinguishable from a determinate progress bar at 12%. The rule
235 is decorative and carries **no** contrast floor: it is deliberately
236 absent from rastrillo's contrast pair table, and a later pass must not
237 argue it up to 1.4.11's 3:1.
238- 300-line JS module cap, test-enforced, ratchet-down only. The vendored
239 Jspreadsheet blob is exempt and named as such; nothing we write is.
240- Every drag-only affordance has a menu equivalent.
241- **Light/dark is per person, not per document**, and must resolve before
242 first paint β€” cell fills resolve against the theme, so a late preference
243 repaints every coloured cell. Read it synchronously from a cookie, never
244 a fetch.
245
246**Deploys**
247- One binary, `CGO_ENABLED=0`, cgo-free SQLite. A cgo driver sneaking in is
248 a bug.
249- Review on a deployed canary, never localhost.
250- **Deploy with `carlos deploy`, never `carlos ship` on its own.** `ship`
251 publishes a release and stops there: the release sits unpromoted, no
252 channel points at it, and nothing serves it. `deploy` ships, promotes
253 and waits for the URL β€” one command, and it is the one that tells you
254 when it is actually live. (Paul, 2026-08-31.)
255- **Build shipping binaries with `make release`, never a bare `go build`.**
256 It stamps `rastrillo.BuildVersion` via `-ldflags -X` and refuses a dirty
257 tree. An unstamped binary reports `dev` from `/api/version` forever.
258- A 200 is not a deploy verification, and neither is `X-Carlos-Version`.
259 That header is the **router's adopted release**; it flips the moment the
260 promote lands and says nothing about which binary is running. `GET
261 /api/version` is served by the process itself. Check the process, not
262 the edge.
263- A promote is not a deploy. Until the process cycles, the old binary
264 serves β€” measured at over 90 seconds here on three consecutive deploys,
265 and about ten on another app, so poll rather than sample. A single early
266 read looks like a failed deploy; a single read against an unstamped
267 binary reports `dev` and looks like a successful one.
268- The two facts only disagree when the binary is stamped. Unstamped, they
269 are structurally incapable of disagreeing, so a check comparing them has
270 no failing case and proves nothing while passing.
271
272---
273
274## Authorship
275
276πŸ€– / πŸ‘¨ markers, `Co-Authored-By: Claude …` trailers, published prompt and
277carbon ledgers. AI is a guest with a name tag: opt-in, disclosed, and every
278path works with it switched off.
279
280## Work in a worktree, and catch main up first
281
282Take a worktree for the work. Do not work in the shared checkout.
283
284 git -C <repo> fetch origin
285 git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main
286
287Both halves matter, for different reasons.
288
289**The worktree**, because several sessions share this machine and some
290share this repository, and a checkout has one HEAD, one index and one
291working tree between all of them. A branch switch under somebody else's
292uncommitted work destroys it with nothing on screen to say so, and two
293sessions editing the same paths disagree quietly instead of conflicting
294loudly. A worktree gives the work its own HEAD and its own files, and
295costs a directory.
296
297**From `origin/main`, after a fetch** β€” not from whatever the checkout
298happens to be sitting on. A branch cut from a stale main is a merge
299conflict scheduled for later, against files somebody else has renamed in
300the meantime. A branch cut from another task's branch is worse: it
301carries that task's commits into yours, where they are reviewed as
302though you wrote them, and the two can only be separated by hand.
303
304Remove it when the work has landed β€” `git worktree remove` β€” so that
305`git worktree list` keeps saying something true.
306
307Written down because of 2026-09-17, in `meet`. The shared checkout was
308sitting on one task's branch while holding a *different* task's feature
309uncommitted on disk β€” some four hundred lines, a new migration among
310them β€” put there by a session nobody else knew was working in that tree.
311Two other sessions were meanwhile coordinating over the same checkout by
312asking each other not to touch it, which is the arrangement a worktree
313makes unnecessary. Nothing was lost. Nothing about the arrangement would
314have said so if it had been, and by then the same feature existed twice,
315once on each side of a rename, because the two sessions had branched
316from different starting points.
317