Therapy Pack — IA/Nav + 7.0.0 Version Bump + Analytics Implementation Plan¶
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: Complete the app inversion in the navigation (Editor becomes a primary sidebar destination; the six tools demote under an "Advanced" grouping), bump the release to 7.0.0, and wire the therapy-pack analytics taxonomy (spec §6).
Architecture: Three independent, low-risk areas. Nav: AppSidebar gains a primary "Therapy Pack Editor" entry above the tools + an "Advanced" umbrella label. Version: bump both package.jsons (a drift test guards them); the AppHeader chip, About drawer, footers, and index.html JSON-LD all derive from PRODUCT.version / a build-injected placeholder — no hand-copied literals. Analytics: add the spec §6 events to the Worker allowlist (minimal props that fit the existing data-point layout) and fire track() at the front page, seed, and add-to-editor moments.
Tech Stack: React 19, TypeScript, MUI 7, Hono/Workers (analytics allowlist), Vitest 4.
Global Constraints¶
- Version single source of truth:
package.json(bothpackages/web/frontendandpackages/web/workers— a drift guardsrc/__tests__/productMetadata.test.tsasserts they match).PRODUCT.version(src/config/product.ts) reads frontendpackage.json;index.htmlJSON-LD uses the build-injected__APP_VERSION__placeholder. Do NOT hand-edit version literals anywhere else. - Analytics = privacy allowlist. New events go in
packages/web/workers/src/config/analyticsEvents.tsANALYTICS_EVENTS. NO free text / no pack content / no word text — count-only. New prop keys must map to an existing slot inBLOB_LAYOUT/DOUBLE_LAYOUT(reuse the existingtoolblob slot; otherwise use empty props). Do NOT reuse the PHON-177-reserved funnel names (pack_started/pack_item_completed/pack_completed/pack_abandoned/pack_exported). - Nav: the Editor is reachable via a primary sidebar entry (
onToolSelect('editor')—App_newalready rendersPackEditorforactiveTool === 'editor'). The six tools stay functional, grouped under "Advanced". Keep the existing Build/Analyze/Explore sub-labels. - Test commands (frontend from
packages/web/frontend/, workers frompackages/web/workers/):npm test -- --run <file>,npm run type-check,npm run lint,npm run build.
File Structure¶
packages/web/frontend/package.json(modify) — version → 7.0.0.packages/web/workers/package.json(modify) — version → 7.0.0.packages/web/frontend/src/components/AppSidebar.tsx(modify) — primary Editor entry + Advanced label.packages/web/frontend/src/components/AppSidebar.test.tsx(create/modify) — nav assertions.packages/web/workers/src/config/analyticsEvents.ts(modify) — new events.packages/web/workers/src/config/analyticsEvents.test.tsor the events route test (modify) — allowlist assertions (if such a test exists).packages/web/frontend/src/components/WelcomeView.tsx(modify) —landing_view+pack_seeded+target_resolvedtrack calls.packages/web/frontend/src/components/pack/AddToEditorButton.tsx(modify) —add_to_editortrack call.
Task 1: Bump version to 7.0.0¶
Files:
- Modify: packages/web/frontend/package.json, packages/web/workers/package.json
- Test: packages/web/frontend/src/__tests__/productMetadata.test.ts (existing — must stay green)
Interfaces: none (mechanical).
- [ ] Step 1: Bump both package.json versions
Set "version": "7.0.0" in BOTH packages/web/frontend/package.json and packages/web/workers/package.json (they must match — the drift guard asserts it).
- [ ] Step 2: Run the drift guard + confirm derived surfaces
Run (from packages/web/frontend/):
npm test -- --run src/__tests__/productMetadata.test.ts
npm run type-check
index.html JSON-LD placeholder is intact, and PRODUCT.version (which the AppHeader chip v${PRODUCT.version} and About drawer read) now resolves to 7.0.0. No hand-edit to index.html/AppHeader/About is needed (all derive).
- [ ] Step 3: Commit
git add packages/web/frontend/package.json packages/web/workers/package.json
git commit -m "chore(release): 7.0.0 — Therapy Pack MVP (app inversion) (PHON-170)"
Task 2: AppSidebar — primary Editor entry + "Advanced" grouping¶
Files:
- Modify: src/components/AppSidebar.tsx
- Test: src/components/AppSidebar.test.tsx (create if absent)
Interfaces:
- AppSidebar keeps its props (tools, activeTool, onToolSelect, open, …). In its toolList(showLabels) render, before the mapped tools, render:
- a primary "Therapy Pack Editor" ListItemButton (an editor icon — e.g. Inventory2/Edit), selected={activeTool === 'editor'}, onClick={() => onToolSelect('editor')}, with an accessible name "Therapy Pack Editor";
- a Divider;
- an "Advanced" section label (reuse the existing showSectionLabel styling / a Typography overline) shown when showLabels.
Then the existing tool map is unchanged (its Build/Analyze/Explore sub-labels remain, now visually under "Advanced").
- [ ] Step 1: Write the failing test
Create src/components/AppSidebar.test.tsx (read the current AppSidebar props to construct a minimal tools array — each tool needs id, icon, title, description, color, section, component):
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import AppSidebar from './AppSidebar';
const tools = [
{ id: 'wordLists', icon: <span />, title: 'Word Lists', description: '', color: 'primary.main', section: 'build', component: null },
] as never;
function renderSidebar(activeTool: string | null, onToolSelect = vi.fn()) {
render(
<AppSidebar
tools={tools} activeTool={activeTool} onToolSelect={onToolSelect}
open onToggle={vi.fn()} mobileOpen={false} onMobileClose={vi.fn()}
/>,
);
return onToolSelect;
}
describe('AppSidebar — Editor primary + Advanced grouping', () => {
it('renders a primary Therapy Pack Editor entry that selects the editor', () => {
const onToolSelect = renderSidebar(null);
const editorBtn = screen.getAllByRole('button', { name: /therapy pack editor/i })[0];
fireEvent.click(editorBtn);
expect(onToolSelect).toHaveBeenCalledWith('editor');
});
it('marks the Editor entry active when activeTool is editor', () => {
renderSidebar('editor');
const editorBtn = screen.getAllByRole('button', { name: /therapy pack editor/i })[0];
expect(editorBtn).toHaveAttribute('aria-current', 'page');
});
it('shows the Advanced grouping label', () => {
renderSidebar(null);
expect(screen.getAllByText(/advanced/i).length).toBeGreaterThan(0);
});
});
- [ ] Step 2: Run the test to verify it fails
Run: npm test -- --run src/components/AppSidebar.test.tsx
Expected: FAIL — no Editor entry / Advanced label.
- [ ] Step 3: Implement
In src/components/AppSidebar.tsx, at the top of the <List> inside toolList(showLabels) (before {tools.map(...)}), add the primary Editor entry + Advanced label. Read the file for the exact ListItemButton/ListItemIcon/ListItemText + collapsed-vs-expanded (showLabels) pattern already used for tools, and mirror it. Sketch:
{/* Primary destination: the Therapy Pack Editor (app inversion) */}
<ListItemButton
selected={activeTool === 'editor'}
aria-current={activeTool === 'editor' ? 'page' : undefined}
onClick={() => onToolSelect('editor')}
sx={{ minHeight: 48, justifyContent: showLabels ? 'initial' : 'center', px: showLabels ? 1.5 : 1, borderRadius: 1, mb: 0.5,
borderLeft: activeTool === 'editor' ? '4px solid' : '4px solid transparent',
borderColor: activeTool === 'editor' ? 'primary.main' : 'transparent' }}
>
<ListItemIcon sx={{ minWidth: 0, mr: showLabels ? 2 : 'auto', justifyContent: 'center' }}>
<Inventory2Icon />
</ListItemIcon>
{showLabels && <ListItemText primary="Therapy Pack Editor" />}
</ListItemButton>
<Divider sx={{ my: 1, mx: showLabels ? 1 : 0.5 }} />
{showLabels && (
<Typography variant="overline" sx={{ px: 1.5, color: 'text.secondary' }}>
Advanced
</Typography>
)}
Inventory2 as Inventory2Icon from @mui/icons-material, and Typography from @mui/material if not already imported.)
- [ ] Step 4: Run the test to verify it passes
Run: npm test -- --run src/components/AppSidebar.test.tsx
Expected: PASS (3/3).
- [ ] Step 5: Type-check and commit
npm run type-check
git add src/components/AppSidebar.tsx src/components/AppSidebar.test.tsx
git commit -m "feat(nav): primary Therapy Pack Editor entry + Advanced tool grouping (PHON-170)"
Task 3: Analytics taxonomy — allowlist + track wiring¶
Files:
- Modify: packages/web/workers/src/config/analyticsEvents.ts
- Modify: packages/web/frontend/src/components/WelcomeView.tsx
- Modify: packages/web/frontend/src/components/pack/AddToEditorButton.tsx
- Test: an existing analytics/events test if present (workers); the frontend track calls are covered by their component tests.
Interfaces:
- Worker ANALYTICS_EVENTS gains (all count-only; tool reuses the existing blob slot):
- landing_view: { props: {} }
- target_resolved: { props: {} }
- pack_seeded: { props: {} }
- add_to_editor: { props: { tool: 'string' } }
- Frontend track() calls (from lib/analytics.ts):
- WelcomeView mount → track('landing_view').
- WelcomeView.handleBuild (both target box + gallery) → track('target_resolved') then, after createSeededPack, track('pack_seeded').
- AddToEditorButton after a successful add → track('add_to_editor', { tool: <sourceTool of the first item> }).
- [ ] Step 1: Add the events to the allowlist
In packages/web/workers/src/config/analyticsEvents.ts, add to ANALYTICS_EVENTS (after pack_edited):
/** PHON-170: outcome-first front page shown. Count only. */
landing_view: { props: {} },
/** PHON-170: a typed/picked target resolved to a build. Count only. */
target_resolved: { props: {} },
/** PHON-170: a pack was seeded from a target. Count only. */
pack_seeded: { props: {} },
/** PHON-170: results added to a pack from a tool. `tool` = source tool. */
add_to_editor: { props: { tool: 'string' } },
add_to_editor's tool prop maps to the existing blob4 = props.tool slot — no BLOB_LAYOUT change. If a workers test enumerates the allowlist, update its expected set.
- [ ] Step 2: Verify workers
Run (from packages/web/workers/):
npm test -- --run
npm run type-check
add_to_editor props validated against the existing layout).
- [ ] Step 3: Wire the frontend track calls
In src/components/WelcomeView.tsx:
- add useEffect(() => { track('landing_view'); }, []); (import track from ../lib/analytics);
- in handleBuild, after usePackStore.getState().createSeededPack(...) succeeds, add track('target_resolved'); track('pack_seeded'); (before onSelectTool('editor')).
In src/components/pack/AddToEditorButton.tsx, in the success branch (after addItems(items)), add:
track('add_to_editor', { tool: items[0]?.provenance.sourceTool ?? 'manual' });
track from ../../lib/analytics.)
- [ ] Step 4: Run the affected frontend tests + full gate
Run (from packages/web/frontend/):
npm test -- --run src/components/WelcomeView.test.tsx src/components/pack/AddToEditorButton.test.tsx
npm test -- --run
npm run type-check
npm run lint
npm run build
MetadataErrorAlert flake aside). The WelcomeView/AddToEditor tests may mock lib/analytics; if they assert on track, update expectations to allow the new calls (they should not break existing assertions).
- [ ] Step 5: Commit
git add packages/web/workers/src/config/analyticsEvents.ts packages/web/frontend/src/components/WelcomeView.tsx packages/web/frontend/src/components/pack/AddToEditorButton.tsx
git commit -m "feat(analytics): therapy-pack taxonomy — landing_view/target_resolved/pack_seeded/add_to_editor (PHON-170)"
Self-Review notes (coverage vs spec §6 / §7)¶
- Editor as primary linkable surface + tools under "Advanced": Task 2. ✓ (
activeTool === 'editor'already rendersPackEditor; the sidebar now surfaces it primarily.) - 7.0.0 version bump (package.json single source; chip/About/JSON-LD derive): Task 1. ✓
- Analytics events (
landing_view,target_resolved,pack_seeded,add_to_editor, plus existingpack_edited+results_exportedfor export): Task 3. ✓ - Privacy: all new events count-only; only
add_to_editor.tool(an enum-ish source-tool string), no pack content. ✓ - Deferred / not needed: a bookmarkable editor route (the sidebar entry + pack indicator suffice for 7.0.0; a URL route is a later nicety);
exportas a distinct event (reusing the allowlistedresults_exportedwithtool: 'therapyPack'from the export phase); the PHON-177 funnel events (reserved, not part of this MVP).target_resolved'smethod(box|gallery|tool) detail dropped to keep props within the existing layout — count-only for now.
Decisions resolved at planning¶
- Both
package.jsons bump together (drift guard) — 7.0.0; everything user-visible derives fromPRODUCT.version/ the build placeholder. - Primary Editor sidebar entry + single "Advanced" umbrella over the existing tool sub-sections — completes the app-inversion nav without a two-level restructure.
- Minimal-prop analytics — new events are count-only (or
tool-only, reusing the existing slot) to avoidBLOB_LAYOUT/DOUBLE_LAYOUTsurgery; richer props are a follow-up.