oficina / slides Public

Nest concern packages under internal/slides

rastrillo migration check requires exactly one directory under
internal/, so internal/core moves to internal/slides/core and the plan's
file map follows (deck, render, present will nest the same way).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01563XGCgxcSZToEXwuTBsKE
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 992e9e7caba4e189e07e99b189b7d05f78703c3b parent 8314eb5
4 files changed, +12 −12
  • docs/superpowers/plans/2026-09-07-slice-1-live.md +11 −11
  • internal/{ => slides}/core/core.go +0 −0
  • internal/{ => slides}/core/core_test.go +1 −1
  • internal/{ => slides}/core/testdata/slides_core.wasm binary

Intent

Human

<task-notification> <task-id>ae8f0dac508d3fbb4</task-id> <tool-use-id>toolu_01MXCPPcnDLkFTfvEuLrzz3x</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-oficina/2420c44b-a27a-4e0d-bbe7-8b3ca0464c21/tasks/ae8f0dac508d3fbb4.output</output-file> <status>completed</status> <summary>Agent "Finish Task 1 fix round 1" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>Report appended correctly. Everything is verified and committed. Status: DONE_WITH_CONCERNS Commit: 8314eb5 "fix: sign-in review round 1" Tests: all covering tests pass (go test ./internal/slidestest/ -run 'Signin|SignedOut|Verify|Development|User|Oracle' and go test ./internal/slides/... — 19 tests, all PASS); `make ci`'s vet/test targets pass, migration-check fails but confirmed pre-existing on branch HEAD (b745b2f, Task 2's cherry-picked internal/core), unrelated to this round. Concerns: `make ci`'s migration-check step fails ("expected exactly one app package under ./internal, found [core slides]") — verified via git stash that this predates this fix round entirely (Task 2's cherry-pick caused it); out of scope since core/ and internal/core/ are off-limits this round. Flagged for the controller to reconcile. Report: /home/paulca/amadan.net/oficina/slides/.claude/worktrees/slice-1/.superpowers/sdd/2026-09-07-slice-1-live/task-1-report.md</result> <usage><subagent_tokens>93842</subagent_tokens><tool_uses>24</tool_uses><duration_ms>130032</duration_ms></usage> </task-notification>

diff --git a/docs/superpowers/plans/2026-09-07-slice-1-live.md b/docs/superpowers/plans/2026-09-07-slice-1-live.md
index f5ce616..8ba8f1f 100644
--- a/docs/superpowers/plans/2026-09-07-slice-1-live.md
+++ b/docs/superpowers/plans/2026-09-07-slice-1-live.md
@@ -13,7 +13,7 @@
## Global Constraints
- `CGO_ENABLED=0` everywhere; never import `github.com/glebarez/*` or `gorm.io/driver/sqlite`.
-- Module path `amadan.net/oficina/slides`; app package `internal/slides`; the five-file shape plus one package per concern (`internal/core`, `internal/deck`, `internal/render`, `internal/present`).
+- Module path `amadan.net/oficina/slides`; app package `internal/slides`; the five-file shape plus one package per concern nested under the app package (`internal/slides/core`, `internal/slides/deck`, `internal/slides/render`, `internal/slides/present`), because `rastrillo migration check` requires exactly one directory under `internal/`.
- Every owned query goes through the `owned(r)` seam; not-found and not-yours both 404.
- Every state-changing route is a POST behind `csrf.Protect`; signed-in routes sit in a chi group with `authPlugin.RequireSession`; `/s/{token}`, `/p/*`, `/r/*` and `/api/*` are mounted outside that group.
- Under `rastrillo/auth` the Subject is the email. Never call `sessions.UserID`; map the address to the `users` row first.
@@ -44,11 +44,11 @@
| `internal/slides/templates/*.html` | One file per page. |
| `internal/slides/static/slides.css` | Slide layout CSS: templates, canvas frame, phone reflow, themes. |
| `internal/slides/locales/en.toml` | Every string. |
-| `internal/core/core.go`, `internal/core/testdata/slides_core.wasm` | wazero host for the Rust core. |
+| `internal/slides/core/core.go`, `internal/slides/core/testdata/slides_core.wasm` | wazero host for the Rust core. |
| `core/` | Rust crate `slides-core`: schema, ops, JSON view, ABI. |
-| `internal/deck/deck.go` | Go types for the JSON view (`Deck`, `Slide`, `Block`, `Text`), `Parse([]byte)`. |
-| `internal/render/render.go`, `themes.go` | `Slide(d, i, vp) template.HTML`, `ThemeVars(id) template.CSS`. |
-| `internal/present/hub.go` | In-memory SSE hub keyed by session id. |
+| `internal/slides/deck/deck.go` | Go types for the JSON view (`Deck`, `Slide`, `Block`, `Text`), `Parse([]byte)`. |
+| `internal/slides/render/render.go`, `themes.go` | `Slide(d, i, vp) template.HTML`, `ThemeVars(id) template.CSS`. |
+| `internal/slides/present/hub.go` | In-memory SSE hub keyed by session id. |
| `scripts/build-wasm.sh`, `scripts/run-dev.sh` | Build the core; run locally with a logged magic link. |
| `.amadan/ci.d/10-gate` | The one gate: cargo test, wasm rebuild and `cmp`, go vet/test/fmt, migration check. |
@@ -141,13 +141,13 @@
### Task 3: Decks: persistence, list, create, sample deck
**Files:**
-- Create: `internal/slides/decks.go`, `internal/slides/sample.go`, `internal/slides/templates/decks.html`, `internal/slides/templates/deck_new.html`, `internal/slides/templates/deck_settings.html`, `internal/deck/deck.go`, `internal/deck/deck_test.go`
+- Create: `internal/slides/decks.go`, `internal/slides/sample.go`, `internal/slides/templates/decks.html`, `internal/slides/templates/deck_new.html`, `internal/slides/templates/deck_settings.html`, `internal/slides/deck/deck.go`, `internal/slides/deck/deck_test.go`
- Modify: `internal/slides/models.go`, `internal/slides/migrations/` (generate `0002_decks.sql`), `internal/slides/app.go`, `internal/slides/render.go`, `internal/slides/locales/en.toml`
- Test: `internal/slidestest/decks_test.go`
**Interfaces:**
- Models: `Deck{ID int64; Sqid string (unique); Title string; OwnerID int64 (index); Rung string ("private"|"team"); Theme string; Seq int64; CreatedAt, UpdatedAt}`, `DeckUpdate{ID int64; DeckID int64 (index); Seq int64; Actor string; Bytes []byte; CreatedAt}` with unique `(deck_id, seq)`, `DeckGrant{ID; DeckID (index); UserID (index); Actor string; CreatedAt}`.
-- `internal/deck`: `type Deck struct{Meta Meta; Slides []Slide; Assets map[string]Asset}`, `Slide{ID, Layout, Template string; Background Background; Phone, Transition string; Notes Text; Blocks []Block}`, `Block{ID, Type string; Props map[string]any; Region string; Box *Box; Z int; Phone PhoneOverride; Text Text}`, `Text [][]Run`, `Run{T string; B, I, C, H bool; Href string}`, `Box{X, Y, W, H float64}`, `PhoneOverride{Hidden bool; Order *int; Size string; Box *Box}`; `Parse(b []byte) (*Deck, error)`; `(Text).Plain() string`.
+- `internal/slides/deck`: `type Deck struct{Meta Meta; Slides []Slide; Assets map[string]Asset}`, `Slide{ID, Layout, Template string; Background Background; Phone, Transition string; Notes Text; Blocks []Block}`, `Block{ID, Type string; Props map[string]any; Region string; Box *Box; Z int; Phone PhoneOverride; Text Text}`, `Text [][]Run`, `Run{T string; B, I, C, H bool; Href string}`, `Box{X, Y, W, H float64}`, `PhoneOverride{Hidden bool; Order *int; Size string; Box *Box}`; `Parse(b []byte) (*Deck, error)`; `(Text).Plain() string`.
- `decks.go`: `(*app).openDeck(ctx, deckID) (*core.Handle, *deck.Deck, error)` loads all `DeckUpdate` rows in seq order into a fresh handle (one wazero module per request, closed by the caller); `(*app).applyOps(ctx, d *Deck, actor string, ops []byte) (*deck.Deck, error)` runs `Ops`, inserts `DeckUpdate{Seq: d.Seq+1}` and bumps `Deck.Seq` in one transaction, and notifies the deck hub (Task 6 adds the hub; here a no-op func field `onUpdate func(deckID int64, seq int64)`).
- `visibleDecks(r) *gorm.DB` — decks where owner is me, or rung is `team`, or a grant names me.
- Sqids: use `github.com/sqids/sqids-go` with a fixed alphabet and min length 8, from the deck's `ID`, as Docs does (check Docs' `sqid.go` for the exact alphabet and reuse it).
@@ -163,8 +163,8 @@
### Task 4: The HTML renderer and themes
**Files:**
-- Create: `internal/render/render.go`, `internal/render/themes.go`, `internal/render/render_test.go`, `internal/render/testdata/*.golden.html`, `internal/slides/static/slides.css`
-- Test: `internal/render/render_test.go`
+- Create: `internal/slides/render/render.go`, `internal/slides/render/themes.go`, `internal/slides/render/render_test.go`, `internal/slides/render/testdata/*.golden.html`, `internal/slides/static/slides.css`
+- Test: `internal/slides/render/render_test.go`
**Interfaces:**
- `render.Viewport` is `Desktop` or `Phone`. `render.Slide(d *deck.Deck, index int, vp Viewport) (template.HTML, error)` returns one `<section slides-slide data-layout=.. data-template=.. data-index=..>` with the theme's variables inline as `style="--slides-bg:..;--slides-fg:..;--slides-accent:..;--slides-font-heading:..;--slides-font-body:.."`, blocks in order, canvas blocks positioned with `style="left:%;top:%;width:%;height:%;z-index"`, phone overrides as `data-phone-hidden`, `data-phone-order`, `data-phone-size`, `data-phone-box` and, when `vp == Phone` and the slide is `phone: reflow`, rendered as flow.
@@ -196,9 +196,9 @@
### Task 6: Presentation sessions, presenter view, phone remote
**Files:**
-- Create: `internal/present/hub.go`, `internal/present/hub_test.go`, `internal/slides/presentation.go`, `internal/slides/templates/present.html`, `internal/slides/templates/present_notes.html`, `internal/slides/templates/remote.html`, `internal/slides/templates/remote_pair.html`, `internal/slides/static/present.js` (under 120 lines: keyboard, swipe, SSE apply; the page is complete without it)
+- Create: `internal/slides/present/hub.go`, `internal/slides/present/hub_test.go`, `internal/slides/presentation.go`, `internal/slides/templates/present.html`, `internal/slides/templates/present_notes.html`, `internal/slides/templates/remote.html`, `internal/slides/templates/remote_pair.html`, `internal/slides/static/present.js` (under 120 lines: keyboard, swipe, SSE apply; the page is complete without it)
- Modify: `models.go` (`Presentation{ID; Sqid string (unique); DeckID (index); Actor string; Slide int; Blackout bool; Code string (unique among live); RemoteTokenHash string; StartedAt; EndedAt *time.Time}`), migrations (`0004_presentations.sql`), `app.go`, `viewer.html` (a "Present" button), `decks.html`
-- Test: `internal/slidestest/present_test.go`, `internal/present/hub_test.go`
+- Test: `internal/slidestest/present_test.go`, `internal/slides/present/hub_test.go`
**Interfaces:**
- `present.Hub`: `New() *Hub`, `(*Hub).Subscribe(key string) (ch <-chan Event, cancel func())`, `(*Hub).Publish(key string, e Event)`, `Event{Slide int; Blackout bool; Ended bool}`. Buffered channels of 8; a slow subscriber is dropped. Copy the ping/deadline/TTL recipe from Docs' `stream.go` (15 s `: ping`, 30 s write deadline, 5 minute TTL then the client reconnects).
diff --git a/internal/core/core.go b/internal/slides/core/core.go
similarity index 100%
rename from internal/core/core.go
rename to internal/slides/core/core.go
diff --git a/internal/core/core_test.go b/internal/slides/core/core_test.go
similarity index 99%
rename from internal/core/core_test.go
rename to internal/slides/core/core_test.go
index 2388cf8..0bff4e7 100644
--- a/internal/core/core_test.go
+++ b/internal/slides/core/core_test.go
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
- "amadan.net/oficina/slides/internal/core"
+ "amadan.net/oficina/slides/internal/slides/core"
)
func newRuntime(t *testing.T) (context.Context, *core.Runtime) {
diff --git a/internal/core/testdata/slides_core.wasm b/internal/slides/core/testdata/slides_core.wasm
similarity index 100%
rename from internal/core/testdata/slides_core.wasm
rename to internal/slides/core/testdata/slides_core.wasm