A **Macroplan** is a week-granular view of the features we've committed to deliver — a simpler cousin of a Gantt chart whose purpose is not just scheduling but *learning* from how our estimates held up over time.
A **Macroplan** is a week-by-week plan that keeps the receipts: what we promised to deliver, when it actually landed, and what the gap taught us — a simpler cousin of a Gantt chart whose purpose is not just scheduling but *learning*.
## Language
**Macroplan**:
A week-granular plan of the **Features** we've committed to deliver, read as a record of how estimates held against reality.
A week-by-week plan that keeps the receipts on the **Features** we promised: when each actually landed, and what the gap taught us.
A week-granular, **learning-oriented** view of the features a team has committed to deliver — a simpler cousin of a Gantt chart whose point is not just scheduling but learning from *how our estimates held up over time*.
A week-by-week plan that keeps the receipts on what a team promised to deliver — a simpler cousin of a Gantt chart whose point is not just scheduling but learning from *how our estimates held up over time*.
Each feature reads left-to-right as a story: where it started, what we first promised, every time it slipped, and when it actually shipped — judged honestly against the **first** estimate, never a moved goalpost.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let the app hold several named Macroplans in localStorage — switch between them, create and delete them, and download any plan as a portable `.toml` file.
**Architecture:**`useMacroplan` becomes the owner of a `Library` (a `{version, activeId, plans[]}` object in localStorage), exposing the active plan's authoring state plus switch/create/delete operations. A new presentational `PlanSwitcher.vue` renders the header dropdown. A small `useSourceExport` util downloads the active plan's TOML, reusing a `slugify` helper extracted from the existing PNG exporter.
- Vue 3 `<script setup lang="ts">` for all components; match the existing component style.
- DaisyUI 5 classes for UI (the app pins a light theme); no new dependencies.
- No backend — everything client-side (ADR-0002). localStorage keys: **`macroplan:library`** (new store) and **`macroplan:source`** (legacy, single-source — migrate away from it).
- Vitest defaults to the `node` environment; any test that touches the DOM or `localStorage` MUST start with the line `// @vitest-environment happy-dom` (see `src/components/MacroplanGrid.test.ts`).
- Plan ids come from `crypto.randomUUID()`. Target is modern browsers; no fallback.
- A plan's display **name** is its parsed TOML `title` (cached as last-good); it is **not** a separately edited field.
- **Invariant:** the library always holds ≥ 1 plan.
A header dropdown that lists plans, marks the active one, and emits switch / new / download intents. No storage or parsing logic — props in, events out.
- DOM contract relied on by the test: each plan is an `<a>` inside a `<li>`; the active plan's `<a>` has class `active`; the last two `<a>` items are **New plan** then **Download .toml**.
- [ ]**Step 1: Write the failing test**
Create `src/components/PlanSwitcher.test.ts`:
```ts
// @vitest-environment happy-dom
import{describe,it,expect}from'vitest'
import{mount}from'@vue/test-utils'
importPlanSwitcherfrom'./PlanSwitcher.vue'
constplans=[
{id:'a',name:'Alpha'},
{id:'b',name:'Bravo'},
]
describe('PlanSwitcher',()=>{
it('lists plan names and marks the active one',()=>{
git commit -m "feat(ui): add the plan switcher dropdown"
```
---
### Task 3: `useMacroplan` library refactor
Turn the single-source composable into the Library owner: storage + migration + active-plan binding + autosave, plus the switch/create/delete operations. Keep `resetToSample` exported for now so `App.vue` still compiles — Task 4 removes it.
Replace the static title block with the switcher, add a trash button + confirm modal that deletes the active plan, wire the `.toml` download, and remove the now-retired `resetToSample`.
**Files:**
- Modify (rewrite): `src/App.vue`
- Modify: `src/composables/useMacroplan.ts` (delete the temporary `resetToSample`)
git commit -m "feat(ui): wire the plan switcher, delete confirm and .toml download into the app"
```
---
### Task 5: Documentation
Record the new `Library` term in the ubiquitous language and flip the README now that the library exists.
**Files:**
- Modify: `CONTEXT.md`
- Modify: `README.md`
- [ ]**Step 1: Add the `Library` term to `CONTEXT.md`**
In the `## Language` section, immediately after the **Now line** entry (the block ending `_Avoid_: today marker, cursor`) and before `## Symbols`, insert:
```markdown
**Library**:
The collection of saved **Macroplans** held in the browser's localStorage — the live store. Always holds at least one Macroplan; durability rests on exporting a Macroplan's `.toml` (per ADR-0002), not on the Library itself. Carries no status of its own.
_Avoid_: workspace, project, file list
```
- [ ]**Step 2: Add a Library relationship line to `CONTEXT.md`**
In `## Relationships`, after the first bullet (the one starting `A **Macroplan** contains a flat, author-ordered list...`), add:
```markdown
- The **Library** holds many **Macroplans**, exactly one of which is active (shown in the editor and grid). Each is identified internally by a stable id and labelled by its **title**.
```
- [ ]**Step 3: Update the README Status paragraph**
Replace:
```markdown
**Feature-complete** against the [design](DESIGN.md) and covered by tests — TOML authoring with live reload, the full week × feature grid render, derived on-time/late classification, milestones, and PNG export all work client-side.
Not yet built: a **library** of multiple named plans. Today a single source autosaves to localStorage.
```
with:
```markdown
**Feature-complete** against the [design](DESIGN.md) and covered by tests — TOML authoring with live reload, a **library** of named plans, the full week × feature grid render, derived on-time/late classification, milestones, and PNG + `.toml` export all work client-side.
```
- [ ]**Step 4: Update the README "How it works" persistence bullet**
Replace:
```markdown
- Your source **autosaves to localStorage**; **export a PNG** to share into Slack or a deck.
```
with:
```markdown
- Keep a **library** of named plans in localStorage and switch between them; **export** any plan as a `.toml` file, or the rendered view as a **PNG** to share into Slack or a deck.
```
- [ ]**Step 5: Verify the docs build is unaffected and commit**
Run: `pnpm test`
Expected: PASS (docs-only change; suites still green).
```bash
git add CONTEXT.md README.md
git commit -m "docs: record the Library term and mark the plans library as built"
```
---
## Self-Review
**1. Spec coverage**
- Multiple named plans in localStorage → Task 3 (`Library` model).
**2. Placeholder scan:** No TBD/TODO-as-deliverable, no "add error handling" hand-waves; every code step shows complete code. The one `TODO(task 4)` comment is a deliberate, scheduled removal (Task 4, Step 1), not an unfinished placeholder.
**3. Type consistency:**`slugify` (Task 1) is consumed by `sourceFilename` (Task 1) and unchanged `exportFilename`. `useMacroplan`'s return shape (Task 3) — `source`, `plan`, `error`, `plans`, `activeId`, `selectPlan`, `newPlan`, `deletePlan` — matches exactly what `App.vue` destructures (Task 4). `PlanSwitcher` props (`plans: {id,name}[]`, `activeId: string`) and emits (`select`, `new`, `download`) match the App template bindings. `StoredPlan`/`Library` are internal to `useMacroplan`. Consistent throughout.
**Status:** Approved (brainstorming) → ready for implementation plan
**Realizes:** [ADR-0002](../../adr/0002-local-first-no-backend.md) — *"The live store is localStorage (an in-app library of named Macroplans); the `.toml` file is the portable source of truth, moved in and out via Import/Export."*
## Problem
The app stores exactly one Macroplan, autosaved to a single localStorage key (`macroplan:source`). ADR-0002 always described localStorage as **a library of named Macroplans**, but only the single-plan slice was built. The README advertised a "library of plans" the app never had.
This adds the library: keep several named Macroplans, switch between them, create and delete them, and download any plan as a portable `.toml` file. The `.toml` download is in scope because ADR-0002 makes export the durability mechanism ("durability rests on Export"); a library of N plans in localStorage with no file export would multiply un-backupable state.
## Scope
**In:**
- Multiple named Macroplans persisted in localStorage.
- A header dropdown to switch the active plan.
- Create a new plan (seeded from the bundled sample).
- Delete the active plan (with a confirm).
- Download the active plan's source as `<title-slug>.toml`.
- Migration from the legacy single-key store.
**Out (YAGNI for v1):**
- Duplicate / clone a plan (fork via *New* + paste).
- File-open `.toml`*import* — pasting a `.toml` file's contents into a new plan's editor already covers loading. Re-evaluate if it proves clumsy.
- Reordering, folders, search, tags.
- Any backend or multi-device sync (explicitly rejected by ADR-0002).
## Vocabulary
A new term enters the [ubiquitous language](../../../CONTEXT.md):
> **Library** — the collection of saved **Macroplans** held in the browser's localStorage. The live store; durability rests on exporting a plan's `.toml` (per ADR-0002). Not a Macroplan itself, and carries no status of its own.
A plan's **display name** is its TOML `title` (the existing concept), not a separate field — see Data Model.
## Data Model
One new localStorage key **replaces**`macroplan:source`:
| `id` | Stable identity from `crypto.randomUUID()`. The switcher and `activeId` key off this, **never** the name — so a plan survives title edits and broken TOML. |
| `name` | The **cached last-good title** for that plan. Lets the switcher label every plan without parsing all of their sources on every render — only the active plan is parsed live. |
| `source` | The TOML string the editor binds to. |
**Why a cached name and not a live-derived one:** only the active plan's source is parsed continuously (for the live render). Inactive plans show their last-good title from `name`. The active plan's `name` is refreshed whenever its source parses to a title; a transient parse error leaves the last-good `name` (and the last-good render) in place — same forgiving behavior as today's F3.
### Invariant
**The library always holds ≥ 1 plan.** Deleting the last plan creates a fresh sample plan rather than leaving an empty store. This keeps `activeId` always valid and the editor always bound to something.
### Migration
On load, `useMacroplan` resolves the initial library:
1. If `macroplan:library` exists and parses → use it.
2. Else if the legacy `macroplan:source` exists → wrap that source as a single plan (`name` = its parsed title, or `"Untitled"` if it doesn't parse), mark it active, write `macroplan:library`, and **remove** the legacy key.
3. Else → seed one sample plan, active.
A corrupt/unparseable `macroplan:library` value is treated as case 3 (seed fresh) rather than throwing — autosave is best-effort, consistent with the existing try/catch around `localStorage.setItem`.
## Architecture
### `useMacroplan` (refactor) — the library owner
Holds `plans` and `activeId`; persists the whole library to `macroplan:library` on any change (best-effort, in a try/catch as today).
Exposes:
| Member | Type | Notes |
|--------|------|-------|
| `source` | writable `Ref<string>` | Reads/writes the **active** plan's `source`. The editor's `v-model` binds here unchanged. Writing it refreshes the active plan's cached `name` when the new source parses to a title. |
| `plan` | `ComputedRef<Plan \| null>` | Active plan's derived model, falling back to its `lastGood` (unchanged behavior). |
| `error` | `ComputedRef<string \| null>` | Active plan's current parse error. |
| `plans` | `ComputedRef<{ id, name }[]>` | For the switcher. |
| `activeId` | `Ref<string>` | Current selection. |
| `newPlan()` | → `void` | Append a sample-seeded plan and switch to it. |
| `deletePlan(id)` | → `void` | Remove; if it was active, re-point `activeId` to the **preceding** plan in the list (or the new first plan if it was at index 0); if it was the last remaining plan, seed a fresh sample (the invariant). |
| `selectPlan(id)` | → `void` | Switch the active plan and **reset `lastGood`** so a broken target never shows the previous plan's render. |
`resetToSample` is **removed** — `newPlan()` (seeded from the sample) replaces it.
- Renders a DaisyUI dropdown: the active plan's name as the trigger; a list of plans (active one checked); then `+ New plan` and `⤓ Download .toml` entries.
- No storage or parsing logic — it only reflects props and emits intent.
### `downloadSource` + `slugify` (new util) — `.toml` export
- Refactor `exportFilename` (in `usePngExport.ts`) to build on an extracted `slugify(title): string` helper, so PNG and `.toml` names stay consistent:
-`exportFilename(title)` → `macroplan-<slug>.png` (unchanged output; existing test still passes).
-`downloadSource(source: string, filename: string)`: `Blob(['…'], { type: 'text/plain' })` → object URL → anchor click → revoke. Mirrors the existing `downloadPng` mechanics. Lives in a small util (e.g. `src/composables/useSourceExport.ts`) — kept separate from `usePngExport` since it's source export, not render export.
### App shell (`App.vue`)
- The static `<h1>Macroplan</h1>` + subtitle block in the header is **replaced** by `<PlanSwitcher>` (logo kept).
- A **trash** button is added to the top bar; it deletes the **active** plan behind a confirm modal (`Delete "<name>"?` · Cancel / Delete). DaisyUI modal or `<dialog>`.
-`Copy PNG` and `Download` (PNG) buttons are **unchanged** — `.toml` download lives in the switcher dropdown precisely so there is no competing "Download" label in the top bar.
- **localStorage write fails** (full/blocked): swallowed in try/catch; the in-memory library stays usable for the session — same posture as today's autosave.
- **Corrupt `macroplan:library` on read:** fall back to seeding a fresh sample (don't throw).
- **Active plan source unparseable:** the grid keeps the last-good render and shows the error in the editor — existing behavior, now per active plan.
- **`crypto.randomUUID` unavailable:** target is modern browsers (the app already uses Clipboard API, `color-mix`, dynamic import); no fallback needed. Note in the plan if a wider support floor is required.
## Testing (Vitest, matching the existing suite)
**`useMacroplan` / library model:**
- No storage → library seeds one sample plan, active.
- Legacy `macroplan:source` present → migrates to a one-plan library (name = parsed title), legacy key removed.
- Corrupt `macroplan:library` → seeds fresh sample (no throw).
-`newPlan()` appends a sample plan and makes it active.
-`deletePlan(active)` removes it and re-points `activeId` to a remaining plan.
-`deletePlan` of the **last** plan re-seeds a fresh sample (invariant holds).
- Editing `source` updates the active entry and refreshes its cached `name` on a valid title; a broken edit keeps the last-good `name` and render.
-`selectPlan` swaps `source`/`plan`/`error` and resets `lastGood` (broken target shows its own error, not the prior render).
**Filename util:**
-`sourceFilename(title)` slug mirrors the existing `exportFilename` test (e.g. `"Q3 Delivery"` → `macroplan-q3-delivery.toml`); empty/garbage title → `macroplan-plan.toml`.
**Component (`@vue/test-utils`):**
-`PlanSwitcher` renders the plan names, marks the active one, and emits `select` / `new` / `delete` / `download` on the right interactions.
## Docs to update (in the implementation plan)
- **`CONTEXT.md`** — add the **Library** glossary entry above.
- **`README.md`** — flip the *"Not yet built: a library …"* note now that it exists; the *How it works* localStorage bullet can mention named plans + `.toml` export.
## Open questions
None blocking. Possible future follow-ups (out of scope): `.toml` file-open import, duplicate-plan, and reordering.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.