ee1fa6bee5
- Create types/ directory with centralized domain types: session, tool-instance, tool-type, git-repository, config-folder, tool-config, project, user, api-response - Remove inline type definitions from API modules; re-export from types/ for backward compatibility - Update state/sessions.tsx to import Session from types/session.ts - Update all consumer components/pages to import from types/ - Extract seed_builtin_tool_types from main.py to seeds/builtin_tool_types.py - Create types/index.ts barrel export Quality gates: tsc (pass), eslint (pass), Python syntax (pass)
330 lines
14 KiB
Markdown
330 lines
14 KiB
Markdown
# Spec: Repository Restructuring and Modularization
|
||
|
||
## Overview
|
||
|
||
Restructure the Headquarter monorepo into a modular, maintainable architecture where every source file has a single responsibility and stays within 300 lines (target: 100–200). No behavior changes. No new features. Pure structural reorganization with standardized naming conventions.
|
||
|
||
**Scope:** Frontend (`apps/web/src/`) and backend (`apps/api/src/`)
|
||
**Non-goals:** Technology swaps, feature additions, database migrations, CI/CD changes
|
||
**Target file size:** 100–200 lines; hard limit 300 lines
|
||
|
||
---
|
||
|
||
## Naming Conventions (MUST follow)
|
||
|
||
| Layer | File Naming | Component/Function Naming | Example |
|
||
|-------|------------|--------------------------|---------|
|
||
| React page components | PascalCase matching exported name | `GitHistoryPage` | `GitHistoryPage.tsx` |
|
||
| React feature components | PascalCase matching exported name | `FileBrowser` | `FileBrowser.tsx` + `FileBrowser.module.css` |
|
||
| React UI primitives | PascalCase matching exported name | `Button`, `Dialog` | `Button.tsx` + `Button.module.css` |
|
||
| React hooks | camelCase | `useTheme`, `useApiQuery` | `useTheme.ts` |
|
||
| Frontend API modules | kebab-case | — | `tool-configs.ts` |
|
||
| Frontend utilities | kebab-case | camelCase functions | `terminal-protocol.ts` |
|
||
| Frontend types | kebab-case | PascalCase interfaces | `session.ts` |
|
||
| CSS modules | kebab-case matching component | — | `file-browser.module.css` |
|
||
| Backend routers | snake_case | snake_case handlers | `tool_instances.py` |
|
||
| Backend services | snake_case | snake_case functions | `docker_compose.py` |
|
||
| Backend models | snake_case | PascalCase classes | `tool_instance.py` |
|
||
| Backend tests | snake_case prefixed with `test_` | — | `test_tool_instances.py` |
|
||
|
||
**CSS Modules rule:** Every React component with significant styling gets its own `.module.css` file. Global styles live in `styles/` and only contain resets, tokens, and layout foundations.
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
|
||
### AC-1: No Monolithic Files Remain
|
||
|
||
**GIVEN** the codebase after restructuring
|
||
**WHEN** we count lines in every `.ts`, `.tsx`, `.py`, and `.css` file under `src/`
|
||
**THEN** no file exceeds 300 lines
|
||
**AND** the average file size under each domain directory is under 200 lines
|
||
|
||
**Test:** Run `find src -type f | xargs wc -l | sort -rn` — verify top result ≤ 300.
|
||
|
||
### AC-2: Types Are Centralized and Deduplicated
|
||
|
||
**GIVEN** a domain type such as `Session` or `ToolInstance`
|
||
**WHEN** a developer searches for its definition
|
||
**THEN** exactly one definition exists under `types/`
|
||
**AND** `api/sessions.ts` and `state/sessions.tsx` both import from `types/session.ts`
|
||
**AND** no API module defines types inline
|
||
|
||
**Test:** Search for `interface Session` — expect 1 result. Search for `interface ToolInstance` — expect 1 result.
|
||
|
||
### AC-3: Styles Are Modular
|
||
|
||
**GIVEN** the frontend build
|
||
**WHEN** `styles.css` is checked
|
||
**THEN** it does not exist (deleted)
|
||
**AND** global styles live in `styles/global.css` (resets + tokens + layout)
|
||
**AND** component styles live in `.module.css` files co-located with components
|
||
**AND** page styles live in `styles/pages/{page-name}.css` for page-specific layout only
|
||
**AND** syntax highlighting styles live in `styles/syntax-highlight.css`
|
||
|
||
**Test:** `test -f src/styles.css` fails. `find src/styles -name "*.css" | wc -l` ≥ 5.
|
||
|
||
### AC-4: Backend Routers Contain Only HTTP Concerns
|
||
|
||
**GIVEN** any router file under `api/`
|
||
**WHEN** reading its contents
|
||
**THEN** it contains only: route definitions, dependency injection, request/response models, and thin handler functions
|
||
**AND** no Docker CLI calls, no Git subprocess calls, no file I/O, no compose file mutation
|
||
**AND** all business logic delegates to `services/` modules
|
||
|
||
**Test:** `grep -n "subprocess\|docker\|compose\|os\." apps/api/src/api/*.py` returns zero matches.
|
||
|
||
### AC-5: Backend Services Are Focused
|
||
|
||
**GIVEN** the `services/` directory
|
||
**WHEN** listing files
|
||
**THEN** each service module has a single responsibility (e.g., container lifecycle, tunnel management, compose generation)
|
||
**AND** `services/docker.py` does not exist (split into focused modules)
|
||
|
||
**Test:** `test -f apps/api/src/services/docker.py` fails. Each `.py` in `services/` is ≤ 300 lines.
|
||
|
||
### AC-6: Inline Components Are Extracted
|
||
|
||
**GIVEN** any page component
|
||
**WHEN** reading its JSX
|
||
**THEN** no inner component definitions exist (no `const FileBrowser = () => ...` inside a page)
|
||
**AND** all extracted components are importable and testable independently
|
||
|
||
**Test:** `grep -rn "const [A-Z].*=.*=>" apps/web/src/pages/` returns zero results.
|
||
|
||
### AC-7: Naming Is Consistent
|
||
|
||
**GIVEN** any source file
|
||
**WHEN** checking its name against the naming table above
|
||
**THEN** it follows the convention for its layer
|
||
**AND** every exported React component matches its file name (case-insensitive)
|
||
|
||
**Test:** Script checks that every `.tsx` file's default/named export matches its basename.
|
||
|
||
### AC-8: All Quality Gates Pass
|
||
|
||
**GIVEN** any phase of the refactor
|
||
**WHEN** running the quality gates
|
||
**THEN** `npm run typecheck` (frontend) passes with zero errors
|
||
**AND** `npm run lint` (frontend) passes with zero warnings
|
||
**AND** `pytest` (backend) passes with zero failures
|
||
**AND** no visual regressions are introduced
|
||
|
||
**Test:** Run all gates after each phase. No failures.
|
||
|
||
### AC-9: Barrel Exports for Stable Boundaries
|
||
|
||
**GIVEN** `components/ui/`, `components/features/{domain}/`, or `types/`
|
||
**WHEN** importing from those directories
|
||
**THEN** an `index.ts` barrel export exists
|
||
**AND** consumers import from the directory, not individual files
|
||
**AND** one-off utilities and API modules do NOT have barrel exports
|
||
|
||
**Test:** `test -f src/components/ui/index.ts` passes. `test -f src/utils/index.ts` fails.
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
### REQ-1: Frontend Type System
|
||
|
||
The system SHALL centralize all shared domain types under `apps/web/src/types/`.
|
||
|
||
**Rationale:** Prevents drift between API types, state types, and component prop types.
|
||
|
||
#### Scenario: Centralizing Session types
|
||
- **GIVEN** `Session` is defined in `api/sessions.ts` and `state/sessions.tsx`
|
||
- **WHEN** the refactor is applied
|
||
- **THEN** a single `types/session.ts` defines the canonical `Session` interface
|
||
- **AND** both `api/sessions.ts` and `state/sessions.tsx` import from it
|
||
- **AND** `api/sessions.ts` no longer exports a `Session` type
|
||
|
||
#### Scenario: API modules lose inline types
|
||
- **GIVEN** `api/tool_types.ts` defines `ToolType` inline
|
||
- **WHEN** the refactor is applied
|
||
- **THEN** `types/tool-type.ts` defines `ToolType`
|
||
- **AND** `api/tool_types.ts` imports and re-exports it
|
||
|
||
### REQ-2: Frontend Style Modules
|
||
|
||
The system SHALL use CSS Modules for component-scoped styles.
|
||
|
||
**Rationale:** Eliminates global CSS specificity wars; makes component styles discoverable and deletable.
|
||
|
||
#### Scenario: Component with styles
|
||
- **GIVEN** `FileBrowser` has custom styles
|
||
- **WHEN** a developer looks for its styles
|
||
- **THEN** they find `components/features/git/file-browser/FileBrowser.module.css`
|
||
- **AND** the module contains only `.fileBrowser` and child selectors
|
||
- **AND** no global class names leak outside the component
|
||
|
||
#### Scenario: Global styles remain minimal
|
||
- **GIVEN** `styles/global.css` exists
|
||
- **WHEN** reading it
|
||
- **THEN** it contains only: CSS variables, `* { box-sizing }`, `body` reset, and shell layout grid
|
||
- **AND** it does not contain component-specific rules (cards, buttons, dialogs, etc.)
|
||
|
||
### REQ-3: Backend Router Separation
|
||
|
||
The system SHALL separate HTTP routing from business logic.
|
||
|
||
**Rationale:** Routers should be thin and testable; business logic should be reusable and independently testable.
|
||
|
||
#### Scenario: Creating a tool instance
|
||
- **GIVEN** a `POST /instances` request
|
||
- **WHEN** the router handles it
|
||
- **THEN** it validates the request body with a Pydantic schema
|
||
- **AND** it calls `services.instances.create_instance(...)`
|
||
- **AND** it returns the response
|
||
- **AND** it does not call `docker compose up`, modify files, or manage tunnels
|
||
|
||
#### Scenario: Starting a tool instance
|
||
- **GIVEN** a `POST /instances/{id}/start` request
|
||
- **WHEN** the router handles it
|
||
- **THEN** it calls `services.instances.lifecycle.start_instance(...)`
|
||
- **AND** it does not contain subprocess calls
|
||
|
||
### REQ-4: Backend Service Focus
|
||
|
||
The system SHALL split `services/docker.py` into single-responsibility modules.
|
||
|
||
**Rationale:** Docker operations span compose, containers, tunnels, and config staging — too many concerns for one file.
|
||
|
||
#### Scenario: Service decomposition
|
||
- **GIVEN** the old `services/docker.py`
|
||
- **WHEN** the refactor is applied
|
||
- **THEN** the following modules exist:
|
||
- `services/docker/compose.py` — compose file generation and modification
|
||
- `services/docker/container.py` — container lifecycle (create, start, stop, remove)
|
||
- `services/docker/tunnel.py` — Cloudflare tunnel management
|
||
- `services/docker/config.py` — config folder staging and file writing
|
||
- **AND** each module is ≤ 300 lines
|
||
- **AND** `services/docker.py` does not exist
|
||
|
||
### REQ-5: Page Component Decomposition
|
||
|
||
The system SHALL split page components into route entry points and feature sub-components.
|
||
|
||
**Rationale:** Pages should orchestrate data and routing, not contain inline UI implementations.
|
||
|
||
#### Scenario: Repo workspace page
|
||
- **GIVEN** the old `pages/repo-workspace.tsx`
|
||
- **WHEN** the refactor is applied
|
||
- **THEN** `pages/repo-workspace/page.tsx` contains only: data loading, layout, and sub-component composition
|
||
- **AND** `components/features/git/file-browser/FileBrowser.tsx` contains the file tree UI
|
||
- **AND** `components/features/git/commit-panel/CommitPanel.tsx` contains the commit form
|
||
- **AND** each extracted component is independently importable
|
||
|
||
#### Scenario: Tool workshop page
|
||
- **GIVEN** the old `pages/tool-workshop.tsx`
|
||
- **WHEN** the refactor is applied
|
||
- **THEN** it is split into:
|
||
- `pages/tool-workshop/page.tsx` — tab navigation and layout
|
||
- `components/features/tool-workshop/ToolTypesTab.tsx`
|
||
- `components/features/tool-workshop/ToolConfigsTab.tsx`
|
||
- `components/features/tool-workshop/ConfigFoldersTab.tsx`
|
||
- **AND** each tab component manages its own form state
|
||
|
||
### REQ-6: Inline Component Extraction
|
||
|
||
The system SHALL not contain inner component definitions.
|
||
|
||
**Rationale:** Inner components cannot be tested independently, cause re-creation on every render, and hide complexity.
|
||
|
||
#### Scenario: No inner components in pages
|
||
- **GIVEN** any file under `pages/`
|
||
- **WHEN** searching for `const [A-Z]` followed by a component body
|
||
- **THEN** zero matches are found
|
||
- **AND** all previously inner components are moved to `components/`
|
||
|
||
### REQ-7: Reusable Loading/Error Patterns
|
||
|
||
The system SHALL extract repeated loading/error/retry UI into shared components.
|
||
|
||
**Rationale:** ~8 pages copy the same `status: "loading" | "ready" | "error"` pattern with identical retry buttons.
|
||
|
||
#### Scenario: Loading state
|
||
- **GIVEN** a page is loading data
|
||
- **WHEN** the UI renders
|
||
- **THEN** it uses `<LoadingState message="Loading sessions..." />` instead of inline JSX
|
||
|
||
#### Scenario: Error state
|
||
- **GIVEN** a page fails to load data
|
||
- **WHEN** the UI renders
|
||
- **THEN** it uses `<ErrorState message="Failed to load" onRetry={loadData} />` instead of inline JSX
|
||
|
||
### REQ-8: Barrel Exports at Stable Boundaries
|
||
|
||
The system SHALL provide `index.ts` barrel exports for stable module boundaries.
|
||
|
||
**Rationale:** Cleaner imports; encapsulates internal file structure.
|
||
|
||
#### Scenario: Importing UI primitives
|
||
- **GIVEN** a developer needs `Button` and `Dialog`
|
||
- **WHEN** they write the import
|
||
- **THEN** they write `import { Button, Dialog } from "@/components/ui"`
|
||
- **AND** not `import { Button } from "@/components/ui/button/button"`
|
||
|
||
#### Scenario: No barrel for utilities
|
||
- **GIVEN** a developer needs `terminal-protocol` utilities
|
||
- **WHEN** they write the import
|
||
- **THEN** they write `import { encodeControlMessage } from "@/utils/terminal-protocol"`
|
||
- **AND** `utils/index.ts` does not exist
|
||
|
||
---
|
||
|
||
## API / Protocol Changes
|
||
|
||
None. This is a pure reorganization refactor. All HTTP endpoints, WebSocket protocols, and database schemas remain unchanged.
|
||
|
||
---
|
||
|
||
## Dependencies
|
||
|
||
No new dependencies required. Existing toolchain:
|
||
- Frontend: React 18, Vite, TypeScript, ESLint, Vitest
|
||
- Backend: FastAPI, SQLAlchemy, Alembic, pytest
|
||
|
||
**Optional consideration:** If CSS Modules are adopted (per proposal), Vite has built-in support — no new dependency needed.
|
||
|
||
---
|
||
|
||
## Non-Functional Requirements
|
||
|
||
- **Build time:** No regression in `npm run build` or `vite build` duration
|
||
- **Bundle size:** No increase in output bundle size
|
||
- **Test runtime:** No regression in `npm run test` or `pytest` duration
|
||
- **Developer experience:** File discovery time (time to locate a component/service) must decrease
|
||
|
||
---
|
||
|
||
## Migration Order
|
||
|
||
| Phase | Concern | Files Touched | Est. Lines |
|
||
|-------|---------|--------------|------------|
|
||
| 1 | Types, seeds, shared deps | `types/`, `main.py`, `repo-workspace.tsx` | ~350 |
|
||
| 2 | Style system | `styles.css` → `styles/` + `.module.css` | ~2,750 |
|
||
| 3a | Backend shared deps | `auth/dependencies.py`, router helpers | ~400 |
|
||
| 3b | `tool_instances` split | `api/tool_instances.py` → router + services | ~800 |
|
||
| 3c | `git_repositories` + `config_profiles` split | Routers + services | ~700 |
|
||
| 4a | `tool-workshop` split | Page + feature components | ~500 |
|
||
| 4b | Pages split | `sessions.tsx`, `dashboard.tsx`, `repo-workspace.tsx` | ~600 |
|
||
| 4c | Naming consistency | Rename files/components | ~350 |
|
||
| 5 | Tests + docs | Backfill tests, naming doc | ~350 |
|
||
|
||
---
|
||
|
||
## Open Questions (Resolved)
|
||
|
||
| # | Question | Resolution |
|
||
|---|----------|------------|
|
||
| 1 | CSS approach | **CSS Modules** — each component gets its own `.module.css` |
|
||
| 2 | Backend API versioning | **Keep flat `api/`** — version when v2 is actually needed |
|
||
| 3 | Feature components location | **`components/features/{domain}/`** |
|
||
| 4 | Barrel exports | **Yes for stable boundaries** (`components/ui/`, `components/features/{domain}/`, `types/`); **no for one-off utilities and API modules** |
|
||
| 5 | Branching strategy | **Merge each phase to `main` immediately** |
|
||
|
||
---
|
||
|
||
*Spec prepared for SDD design phase. Next: technical design with exact file layout and import patterns.*
|