From 6c12d4b0db8610afa4458f50cda604d8f97a623b Mon Sep 17 00:00:00 2001 From: yanweidong Date: Sun, 19 Jul 2026 19:06:42 +0800 Subject: [PATCH] feat: add project workspace data model --- .superpowers/sdd/task-2-report.md | 44 +++++++++ .../src/features/workbench/mockData.test.ts | 29 ++++++ apps/web/src/features/workbench/mockData.ts | 61 ++++++++++++ apps/web/src/features/workbench/types.ts | 93 +++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 .superpowers/sdd/task-2-report.md create mode 100644 apps/web/src/features/workbench/mockData.test.ts create mode 100644 apps/web/src/features/workbench/mockData.ts create mode 100644 apps/web/src/features/workbench/types.ts diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md new file mode 100644 index 0000000..48a37cf --- /dev/null +++ b/.superpowers/sdd/task-2-report.md @@ -0,0 +1,44 @@ +# Task 2 Report: Workbench Types And Project-Scoped Mock Data + +## What changed + +- Added the typed workbench domain model in `apps/web/src/features/workbench/types.ts`. +- Added two project records and project-scoped workspace mock data in `apps/web/src/features/workbench/mockData.ts`. +- Added coverage for dynamic projects, project-specific channels, channel ordering, and custom-link URL shape in `apps/web/src/features/workbench/mockData.test.ts`. +- Kept the implementation scoped to the requested workbench data and test files. No UI components, React dependencies, or shadcn/ui dependencies were changed. + +## Tests + +- Targeted test: `npm test -- --run src/features/workbench/mockData.test.ts` -> 1 file passed, 2 tests passed. +- Full Web test suite: `npm test -- --run` -> 4 files passed, 7 tests passed. +- Web production build: `npm run build` -> successful. +- Formatting/diff validation: `git diff --check` -> no issues. + +## TDD evidence + +1. Created `mockData.test.ts` before production implementation. +2. Ran the targeted test and observed the expected red result: Vitest could not resolve `./mockData` because the implementation files did not exist. +3. Added `types.ts` and `mockData.ts` using the exact task brief values. +4. Re-ran the targeted test and observed green: 2/2 tests passed. +5. Ran the full Web test suite and build successfully. + +## Files changed + +- `apps/web/src/features/workbench/types.ts` +- `apps/web/src/features/workbench/mockData.ts` +- `apps/web/src/features/workbench/mockData.test.ts` +- `.superpowers/sdd/task-2-report.md` + +## Self-review + +- `ChannelType` contains all seven required channel literals in the specified order. +- `ProjectWorkspace` includes all required project-scoped collections. +- Both projects receive distinct channel IDs, item IDs, and custom-link URLs through the project ID interpolation. +- `getProjectWorkspace` returns the requested workspace and preserves the brief's fallback to the first workspace for unknown IDs. +- The test verifies at least two projects, numeric IDs, names, distinct workspace IDs, channel order, and custom-link URL format. +- No unrelated tracked files were changed. + +## Concerns + +- Vitest and Vite report the pre-existing warning that no Svelte config was found in `apps/web`; it did not prevent tests or the build from succeeding. +- The mock data is intentionally static and duplicated across projects via the shared workspace mapper, as required for this MVP task. diff --git a/apps/web/src/features/workbench/mockData.test.ts b/apps/web/src/features/workbench/mockData.test.ts new file mode 100644 index 0000000..fdbf451 --- /dev/null +++ b/apps/web/src/features/workbench/mockData.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { getProjectWorkspace, workbenchProjects } from './mockData'; + +describe('workbench mock data', () => { + it('provides dynamic projects', () => { + expect(workbenchProjects.length).toBeGreaterThanOrEqual(2); + expect(workbenchProjects[0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) }); + }); + + it('loads project-specific channels and counts', () => { + const first = getProjectWorkspace(workbenchProjects[0].id); + const second = getProjectWorkspace(workbenchProjects[1].id); + + expect(first.project.id).not.toBe(second.project.id); + expect(first.channels.map((channel) => channel.type)).toEqual([ + 'overview', + 'inbox', + 'tasks', + 'ai_sessions', + 'notes_sources', + 'cron', + 'custom_link', + ]); + expect(first.channels.find((channel) => channel.type === 'custom_link')).toMatchObject({ + title: expect.any(String), + url: expect.stringMatching(/^https?:\/\//), + }); + }); +}); diff --git a/apps/web/src/features/workbench/mockData.ts b/apps/web/src/features/workbench/mockData.ts new file mode 100644 index 0000000..90a25c2 --- /dev/null +++ b/apps/web/src/features/workbench/mockData.ts @@ -0,0 +1,61 @@ +import type { ProjectWorkspace, WorkbenchChannel, WorkbenchProject } from './types'; + +export const workbenchProjects: WorkbenchProject[] = [ + { id: 1, name: 'Project A1', description: 'Client strategy workspace', initials: 'A1', unreadCount: 36 }, + { id: 2, name: 'Project A2', description: 'Operations planning workspace', initials: 'A2', unreadCount: 12 }, +]; + +function systemChannels(projectID: number): WorkbenchChannel[] { + return [ + { id: `${projectID}-overview`, projectID, type: 'overview', title: 'Overview', icon: 'home', count: 635, sortOrder: 1 }, + { id: `${projectID}-inbox`, projectID, type: 'inbox', title: 'Message Flow', icon: 'mail', count: 36, sortOrder: 2 }, + { id: `${projectID}-tasks`, projectID, type: 'tasks', title: 'Work Plan', icon: 'list', count: 8, sortOrder: 3 }, + { id: `${projectID}-ai`, projectID, type: 'ai_sessions', title: 'AI Sessions', icon: 'sparkles', count: 4, sortOrder: 4 }, + { id: `${projectID}-notes`, projectID, type: 'notes_sources', title: 'Notes & Sources', icon: 'file', count: 343, sortOrder: 5 }, + { id: `${projectID}-cron`, projectID, type: 'cron', title: 'Cron Plans', icon: 'clock', count: 45, sortOrder: 6 }, + { + id: `${projectID}-custom-roadmap`, + projectID, + type: 'custom_link', + title: projectID === 1 ? 'Roadmap Board' : 'Ops Dashboard', + icon: 'link', + url: projectID === 1 ? 'https://example.com/roadmap-a1' : 'https://example.com/ops-a2', + sortOrder: 7, + }, + ]; +} + +const workspaces: ProjectWorkspace[] = workbenchProjects.map((project) => ({ + project, + channels: systemChannels(project.id), + tags: ['all', 'tag1', 'UI', 'knowledge'], + recentSessions: [ + { id: `${project.id}-session-1`, title: 'Skill setup notes', summary: 'Install and verify Codex skills.', updatedAt: 'Yesterday', references: ['Notes'] }, + { id: `${project.id}-session-2`, title: 'Pricing logic analysis', summary: 'Compare decision branches and risks.', updatedAt: '7 days ago', references: ['Tasks', 'Sources'] }, + ], + inbox: [ + { id: `${project.id}-inbox-1`, source: 'Manual', title: 'Collect competitor pricing notes', summary: 'Turn pasted research into structured follow-up tasks.', status: 'open', tag: 'UI', time: '09:32' }, + { id: `${project.id}-inbox-2`, source: 'AI', title: 'Meeting summary candidate', summary: 'Review suggested note before saving official object.', status: 'processed', tag: 'knowledge', time: 'Yesterday' }, + ], + tasks: [ + { id: `${project.id}-task-1`, title: 'Confirm homepage information architecture', summary: 'Review channel layout and right inspector behavior.', completed: false, owner: 'David', due: 'Today', tag: 'UI' }, + { id: `${project.id}-task-2`, title: 'Archive old planning notes', summary: 'Move outdated notes into processed state.', completed: true, owner: 'Team', due: 'Yesterday', tag: 'knowledge' }, + ], + aiSessions: [ + { id: `${project.id}-ai-1`, title: 'UI prototype critique', summary: 'Discuss Discord-like project channel behavior.', updatedAt: '10 min ago', references: ['ScreenShot.png', 'design.md'] }, + { id: `${project.id}-ai-2`, title: 'Task sharing policy', summary: 'Validate explicit object sharing rules.', updatedAt: 'Yesterday', references: ['Tasks'] }, + ], + notesSources: [ + { id: `${project.id}-note-1`, kind: 'note', title: 'Product workbench principles', updatedAt: 'Today', tag: 'knowledge', source: 'Markdown' }, + { id: `${project.id}-file-1`, kind: 'file', title: 'ScreenShot.png', updatedAt: 'Today', tag: 'UI', source: 'Attachment' }, + { id: `${project.id}-link-1`, kind: 'link', title: 'Reference board', updatedAt: '7 days ago', tag: 'tag1', source: 'URL' }, + ], + cronPlans: [ + { id: `${project.id}-cron-1`, title: 'Weekly inbox review reminder', schedule: '0 9 * * 1', nextRun: 'Next Monday 09:00', enabled: true, lastResult: 'Not run yet', owner: 'David' }, + { id: `${project.id}-cron-2`, title: 'Monthly source cleanup', schedule: '0 10 1 * *', nextRun: 'Next month', enabled: false, lastResult: 'Paused', owner: 'Team' }, + ], +})); + +export function getProjectWorkspace(projectID: number): ProjectWorkspace { + return workspaces.find((workspace) => workspace.project.id === projectID) ?? workspaces[0]; +} diff --git a/apps/web/src/features/workbench/types.ts b/apps/web/src/features/workbench/types.ts new file mode 100644 index 0000000..98ae0f9 --- /dev/null +++ b/apps/web/src/features/workbench/types.ts @@ -0,0 +1,93 @@ +export type ChannelType = + | 'overview' + | 'inbox' + | 'tasks' + | 'ai_sessions' + | 'notes_sources' + | 'cron' + | 'custom_link'; + +export type WorkbenchProject = { + id: number; + name: string; + description: string; + initials: string; + unreadCount: number; +}; + +export type WorkbenchChannel = { + id: string; + projectID: number; + type: ChannelType; + title: string; + icon: string; + count?: number; + url?: string; + sortOrder: number; +}; + +export type InboxMessage = { + id: string; + source: string; + title: string; + summary: string; + status: 'open' | 'processed' | 'archived'; + tag: string; + time: string; +}; + +export type WorkTask = { + id: string; + title: string; + summary: string; + completed: boolean; + owner: string; + due: string; + tag: string; +}; + +export type AISessionItem = { + id: string; + title: string; + summary: string; + updatedAt: string; + references: string[]; +}; + +export type NoteSourceItem = { + id: string; + kind: 'note' | 'file' | 'link'; + title: string; + updatedAt: string; + tag: string; + source: string; +}; + +export type CronPlan = { + id: string; + title: string; + schedule: string; + nextRun: string; + enabled: boolean; + lastResult: string; + owner: string; +}; + +export type InspectorItem = { + title: string; + type: string; + description: string; + properties: Array<{ label: string; value: string }>; +}; + +export type ProjectWorkspace = { + project: WorkbenchProject; + channels: WorkbenchChannel[]; + tags: string[]; + recentSessions: AISessionItem[]; + inbox: InboxMessage[]; + tasks: WorkTask[]; + aiSessions: AISessionItem[]; + notesSources: NoteSourceItem[]; + cronPlans: CronPlan[]; +};