Rewrite sprint plan: fit the task to the timescale

Six 2-week sprints, each shipping one or two complete deliverables.
Not 20 weeks crammed into 2 — the right amount of work for the time.
Each sprint is valuable on its own. Stop after any and you've shipped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 16:24:59 +00:00
parent a7cca2f720
commit 105f4c4679

View File

@@ -1,147 +1,240 @@
# Rose Ash: Two-Week Sprint # Rose Ash: Fortnightly Sprints
**The 20-week plan, compressed. Cut everything that isn't code running in production.** **Fit the task to the timescale, not vice versa.**
The golden rule of project management. Don't take a 20-week plan and cram it into 2 weeks — that's denial. Ask instead: what's the highest-value thing you can *finish* — not start, finish — in two weeks?
Each sprint ships one or two deliverables. Both complete, both deployed, both making everything after easier. The master plan is still the map. You walk it two steps at a time.
--- ---
## What Gets Cut ## Sprint 1: Sexp Content + Sexp Wire (Weeks 1-2)
| Deferred | Why | ### Week 1: Posts become sexp
|---|---|
| Rust native client | Needs stable protocol first — build later |
| Client-as-node / IPFS / GPU mesh | Vision is solid, build after protocol exists |
| Browser extension | sexpr.js in a `<script>` tag works today, extension is optimisation |
| Scalability Tiers 1-3 | Tier 0 is enough, scale when traffic demands |
| Multi-instance federation | One instance first, federate second |
| Sexp routes (`defroute`) | Nice-to-have, Quart decorators work fine |
**What stays: everything that produces working infrastructure this fortnight.** Ghost still runs for membership and newsletters. Don't touch that. Just the content path.
--- **Deliverable: every post is sexp in the database, rendered through the sexp evaluator, editable through the existing editor.**
## Week 1: Kill Ghost, Ship Sexp Content - [ ] Lexical → sexp converter script (one-time migration)
### Day 1-2: Ghost Content Migration
- [ ] Write Lexical → sexp converter script
- [ ] Add `body_sexp` column to Post model (Alembic migration) - [ ] Add `body_sexp` column to Post model (Alembic migration)
- [ ] Run converter against all posts, verify HTML output matches - [ ] Run converter against all posts
- [ ] Switch rendering pipeline: `post_data()` reads `body_sexp`, renders via sexp evaluator - [ ] Diff HTML output: Lexical render vs sexp render for every post
- [ ] Fix conversion gaps (embeds, cards, special blocks)
### Day 2-3: Native Editor & Uploads - [ ] Switch rendering pipeline: `post_data()` reads `body_sexp`
- [ ] Editor save endpoint writes sexp directly to DB
- [ ] Editor save endpoint writes sexp directly to DB (bypass Ghost Admin API) - [ ] Native media upload endpoints (image, audio, files) — retarget from Ghost proxy
- [ ] Native media upload endpoints on blog service (image, audio, files)
- [ ] Native OEmbed lookup endpoint - [ ] Native OEmbed lookup endpoint
- [ ] Editor toolbar actions → sexp nodes - [ ] Deploy, verify every page renders correctly
### Day 3-4: Membership Decoupling ### Week 2: Fragment endpoints return sexp
HTTP+HMAC stays as transport. The change is what travels over it.
**Deliverable: `fetch_fragment` returns sexp trees instead of HTML strings. Callers render at the boundary.**
- [ ] Relations service returns raw sexp (already renders from sexp — just stop calling `to_html()`)
- [ ] Blog renders relations sexp to HTML at the route level
- [ ] Events renders relations sexp to HTML at the route level
- [ ] Market renders relations sexp to HTML at the route level
- [ ] Callers can now filter, reorder, transform fragments before rendering
- [ ] Measure: is this faster, slower, or same as HTML fragments?
- [ ] Unit tests for sexp fragment round-trip (parse → filter → render)
### Sprint 1 outcome
Posts are sexp. Internal fragments are sexp. Ghost still runs but only for membership/newsletters — the content path is clean. Everything that follows builds on this.
---
## Sprint 2: Kill Ghost + New Relations (Weeks 3-4)
### Week 3: Membership decoupling
**Deliverable: all Ghost membership/newsletter infrastructure replaced with native equivalents.**
- [ ] Rename `ghost_labels``labels`, drop `ghost_id` - [ ] Rename `ghost_labels``labels`, drop `ghost_id`
- [ ] Rename `ghost_tiers``membership_tiers`, drop `ghost_id` - [ ] Rename `ghost_tiers``membership_tiers`, drop `ghost_id`
- [ ] Clean User model (drop ghost fields, add `membership_tier_id`, `membership_status`) - [ ] Clean User model (drop `ghost_id`, `ghost_status`, `ghost_subscribed`, `ghost_note`, `ghost_raw`)
- [ ] Native `subscriptions` table (replace `ghost_subscriptions`) - [ ] Add `membership_tier_id`, `membership_status` to User
- [ ] Wire Stripe directly (Checkout + Webhooks on orders service) - [ ] Native `subscriptions` table replacing `ghost_subscriptions`
- [ ] Wire Stripe directly on orders service (Checkout + Webhooks)
### Day 4-5: Newsletter + Delete Ghost - [ ] Native newsletter model + `user_newsletters`
- [ ] Email sending via SMTP/SES
- [ ] Native newsletter model + user_newsletters
- [ ] Email sending via SMTP/SES (transactional, not Ghost)
- [ ] Newsletter templates as sexp → email-safe HTML - [ ] Newsletter templates as sexp → email-safe HTML
- [ ] Post → email campaign workflow - [ ] Post → email campaign workflow
- [ ] **Delete all Ghost code** — ghost/ directory, ghost_db, webhooks, env vars, Docker service
### Week 4: Delete Ghost + add entity relations
**Deliverable: Ghost is gone. New entity relations are live.**
- [ ] Delete `blog/bp/blog/ghost/` directory
- [ ] Delete `ghost_db.py`, `web_hooks/`, `ghost_admin_token.py`, `ghost_membership.py`
- [ ] Remove Ghost Docker service, all `GHOST_*` env vars
- [ ] Alembic migration: drop all `ghost_id` columns - [ ] Alembic migration: drop all `ghost_id` columns
- [ ] Rename model files: `ghost_content.py``content.py`, `ghost_membership_entities.py``membership.py` - [ ] Rename model files: `ghost_content.py``content.py`, `ghost_membership_entities.py``membership.py`
- [ ] Add new `defrelation`s: `post->post`, `market->product`, `calendar->calendar_entry`, `page->marketplace`
- [ ] Migrate `CalendarEntryPost` junction → `ContainerRelation`
- [ ] Verify: entire platform runs with zero Ghost code
### Parallel (Week 1): New Relations + Cart Split ### Sprint 2 outcome
- [ ] Add new `defrelation`s (post→post, market→product, calendar→entry, page→marketplace) Ghost is dead. Membership is native. New entity relations work. The platform is self-contained.
- [ ] Migrate CalendarEntryPost junction → ContainerRelation
- [ ] Scaffold likes service (port 8009)
- [ ] Move PageConfig to blog
- [ ] Cart becomes thin CRUD + checkout delegation to orders
--- ---
## Week 2: Sexp Protocol + Client Runtime ## Sprint 3: Cart Split + Sexp Pages (Weeks 5-6)
### Day 6-7: Internal Wire Format ### Week 5: Cart microservices split
- [ ] Define framing: length-prefixed sexp over Unix sockets **Deliverable: cart is thin CRUD. Likes, orders, and PageConfig are in their proper homes.**
- [ ] Python `SexpConnection` class: connect, send, receive
- [ ] Quart handler: accept sexp requests, return sexp responses - [ ] Scaffold likes service (port 8009)
- [ ] Move PageConfig to blog service
- [ ] Orders service owns Order/OrderItem, checkout, Stripe/SumUp webhooks
- [ ] Cart becomes CartItem CRUD + checkout delegation
- [ ] Fragment cleanup: move remaining domain templates out of `shared/templates/`
### Week 6: Sexp page layouts
**Deliverable: page layouts are sexp components, not Jinja template inheritance.**
- [ ] Base page layout as sexp (head, nav, main, footer)
- [ ] Blog post page layout
- [ ] Market product page layout
- [ ] Event calendar page layout
- [ ] Component composition replaces `{% extends "base.html" %}`
- [ ] Content negotiation: same route returns HTML or sexp based on `Accept` header
### Sprint 3 outcome
Services are properly bounded. Pages are sexp top to bottom. The `Accept: application/sexp` header works — the door to client-side sexp is open.
---
## Sprint 4: Internal Protocol (Weeks 7-8)
### Week 7: Python sexp client/server library
**Deliverable: working `SexpConnection` class, Quart handler, drop-in `fetch_sexp()`.**
- [ ] Wire format: length-prefixed sexp over Unix sockets
- [ ] `SexpConnection`: connect, send sexp, receive sexp, handle streams
- [ ] Quart integration: handler accepts sexp requests, returns sexp responses
- [ ] `fetch_sexp()` — unified replacement for `fetch_data()` + `fetch_fragment()` - [ ] `fetch_sexp()` — unified replacement for `fetch_data()` + `fetch_fragment()`
- [ ] Error model: `(err :code 404 :message "not found")`
- [ ] Unit tests for protocol library
### Day 7-8: Internal Mesh on Sexp ### Week 8: Internal mesh migration
- [ ] Blog ↔ relations on sexp protocol **Deliverable: all inter-service communication runs on native sexp protocol.**
- [ ] Events ↔ relations on sexp protocol
- [ ] Market ↔ relations on sexp protocol - [ ] Blog ↔ relations on sexp
- [ ] All internal calls on native sexp (HTTP fallback for external) - [ ] Events ↔ relations on sexp
- [ ] Market ↔ relations on sexp
- [ ] Cart ↔ orders on sexp
- [ ] Account ↔ all services on sexp
- [ ] Benchmark: latency/throughput vs HTTP+HMAC - [ ] Benchmark: latency/throughput vs HTTP+HMAC
- [ ] HTTP kept as fallback for external/third-party calls
### Day 8-9: sexpr.js Core ### Sprint 4 outcome
The protocol is real. Running in production. Carrying every inter-service call. Battle-tested on real traffic.
---
## Sprint 5: sexpr.js (Weeks 9-10)
### Week 9: Core runtime
**Deliverable: sexpr.js parses, renders, and mutates the DOM from sexp.**
- [ ] Sexp parser in JS (<5KB gzipped) - [ ] Sexp parser in JS (<5KB gzipped)
- [ ] DOM renderer: sexp tree → DOM nodes - [ ] DOM renderer: sexp tree → DOM nodes
- [ ] `swap!`, `batch!`, `class!` mutation primitives - [ ] `swap!`, `batch!`, `class!` mutation primitives
- [ ] `request!` — fetch sexp from server, apply mutations - [ ] `request!` — fetch sexp from server, apply mutations
- [ ] Component system: `defcomp`, content-addressed localStorage cache - [ ] Component system: `defcomp`, slots
### Day 9-10: sexpr.js Integration ### Week 10: Integration + caching
**Deliverable: sexpr.js on every rose-ash page, with content-addressed caching.**
- [ ] Rose-ash serves sexp via `Accept: application/sexp` content negotiation
- [ ] `<script src="sexpr.js">` on every page — progressive enhancement - [ ] `<script src="sexpr.js">` on every page — progressive enhancement
- [ ] Partial page updates via sexp mutations (no full reload) - [ ] Partial page updates via sexp mutations (replaces full reload for nav, cart, likes)
- [ ] Content-addressed caching: hash component/post body, cache in localStorage
- [ ] Server sends hash manifest — unchanged content served from cache
- [ ] DevTools: sexp inspector in browser console - [ ] DevTools: sexp inspector in browser console
- [ ] Content-addressed caching: hash post body, cache in localStorage
### Parallel (Week 2): Stability + Pages ### Sprint 5 outcome
- [ ] Sexp page layouts (base, blog post, market product, event calendar) The client speaks sexp. Pages load faster. Navigation doesn't reload. Components cache locally. Tier 1 is live.
- [ ] Fragment endpoints return raw sexp (callers render)
- [ ] Unit tests for new protocol library
- [ ] AP federation test: sexp activities between two instances (Docker)
- [ ] Deploy Tier 0 scalability (DB split, PgBouncer, workers)
--- ---
## End State: Day 14 ## Sprint 6: Federation + Stability (Weeks 11-12)
| Before | After | ### Week 11: AP over sexp
|---|---|
| Ghost CMS running alongside Python stack | Ghost deleted, all content native sexp | **Deliverable: two rose-ash instances federate using sexp activities.**
| Lexical JSON in posts | Sexp in `body_sexp` column |
| Ghost handles newsletters, membership, Stripe | Native newsletter, membership, direct Stripe | - [ ] Sexp-formatted activities (Create, Follow, Like, Accept)
| HTTP+HMAC between services | Native sexp protocol between services | - [ ] Federation test: two instances in Docker, full activity flow
| HTML fragments (opaque strings) | Sexp trees (structured, filterable, cacheable) | - [ ] Identity verification over sexp (RSA signatures in sexp envelope)
| Full page reload on every navigation | sexpr.js partial updates, content-addressed cache | - [ ] Cross-instance content rendering (sexp posts display correctly on remote instance)
| No client-side sexp | sexpr.js running on every page |
| 6 Ghost-related files + Docker service | Zero Ghost code | ### Week 12: Harden + deploy
**Deliverable: everything stable, tested, and deployed.**
- [ ] Unit test coverage for all new code (protocol, converter, newsletter, likes)
- [ ] Deploy Tier 0 scalability (DB split, PgBouncer, Hypercorn workers)
- [ ] Bug sweep: end-to-end test every user flow
- [ ] Performance baseline: measure page load, inter-service latency, cache hit rates
- [ ] Documentation: update CLAUDE.md with new architecture
### Sprint 6 outcome
Platform is stable, federated, performant, and documented. The protocol has been tested across network boundaries.
--- ---
## What "90%" Means ## After Sprint 6: The 10%
The 10% left after two weeks: These are real projects, not sprint tasks. Build them when they're needed:
- Rust native client (Track 8) — needs months of Rust engineering - **Rust client library + native client** — when you want Tier 2 performance
- Client-as-node / IPFS / GPU mesh — needs the Rust client - **Browser extension** — when sexpr.js proves the concept and you want Tier 1.5
- Browser extension — nice optimisation, not essential when sexpr.js works - **Client-as-node / IPFS / GPU mesh** — when the cooperative network has multiple members with hardware
- Multi-instance federation — test with Docker first, real instances later - **Multi-cooperative commerce** — when a second co-op actually exists and wants to federate
- Scalability Tiers 1-3 — premature until traffic demands it - **Scalability Tiers 1-3**when traffic hits Tier 0 limits
- Full newsletter template library — basic sending works, fancy templates iterate
Everything a user touches — browsing, buying, posting, subscribing, federating — runs on sexp end to end. The remaining 10% is optimisation and long-term vision.
--- ---
## Daily Rhythm ## The Rhythm
``` ```
Morning: AI generates code for the day's tasks Each sprint:
Midday: Test, fix, deploy to dev Monday-Thursday: Build. AI generates, you steer and test.
Evening: Commit, push, verify on dev, plan tomorrow Friday: Deploy to dev, verify, fix what broke.
Weekend: Rest or think about next sprint.
Each week:
One deliverable. Finished. Deployed. Working.
``` ```
No PRs, no code review, no standups. One developer, AI tools, `git push`. **12 weeks, 12 deliverables, each one complete.** Not 20 weeks of partial progress. Not 2 weeks of rushed everything. The right amount of work for the time available.
---
## Sprint Schedule
```
Sprint 1 (W1-2): Sexp content + sexp wire ← content path is clean
Sprint 2 (W3-4): Kill Ghost + new relations ← platform is self-contained
Sprint 3 (W5-6): Cart split + sexp pages ← services bounded, pages are sexp
Sprint 4 (W7-8): Internal sexp protocol ← protocol is real and running
Sprint 5 (W9-10): sexpr.js client runtime ← client speaks sexp
Sprint 6 (W11-12): Federation + stability ← federated and stable
```
Each sprint builds on the last. Each is valuable on its own. If you stop after any sprint, you've shipped something real.