Skip to content

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 (both packages/web/frontend and packages/web/workers — a drift guard src/__tests__/productMetadata.test.ts asserts they match). PRODUCT.version (src/config/product.ts) reads frontend package.json; index.html JSON-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.ts ANALYTICS_EVENTS. NO free text / no pack content / no word text — count-only. New prop keys must map to an existing slot in BLOB_LAYOUT/DOUBLE_LAYOUT (reuse the existing tool blob 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_new already renders PackEditor for activeTool === '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 from packages/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.ts or 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_resolved track calls.
  • packages/web/frontend/src/components/pack/AddToEditorButton.tsx (modify) — add_to_editor track 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
Expected: PASS — the frontend/workers versions match, the 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>
        )}
(Import 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
Expected: PASS (the events route DROP-unknown behavior now accepts the 4 new names; 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' });
(import 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
Expected: all pass (the one known pre-existing unrelated 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 renders PackEditor; 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 existing pack_edited + results_exported for 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); export as a distinct event (reusing the allowlisted results_exported with tool: 'therapyPack' from the export phase); the PHON-177 funnel events (reserved, not part of this MVP). target_resolved's method (box|gallery|tool) detail dropped to keep props within the existing layout — count-only for now.

Decisions resolved at planning

  1. Both package.jsons bump together (drift guard) — 7.0.0; everything user-visible derives from PRODUCT.version / the build placeholder.
  2. Primary Editor sidebar entry + single "Advanced" umbrella over the existing tool sub-sections — completes the app-inversion nav without a two-level restructure.
  3. Minimal-prop analytics — new events are count-only (or tool-only, reusing the existing slot) to avoid BLOB_LAYOUT/DOUBLE_LAYOUT surgery; richer props are a follow-up.