fix(arch): Config SSOT, reproducible builds, and data pipeline documentation (#41)
ba32591
unverified
Spec 37.1: Foundation Components
Status: READY FOR IMPLEMENTATION Phase: 1 of 5 Depends On: Spec 37.0 (Project Setup) Goal: TDD implementation of Layout and MetricsPanel components
Deliverables
By the end of this phase, you will have:
Layoutcomponent with header and main content areaMetricsPanelcomponent displaying segmentation results- 100% test coverage for both components
- Visual verification in browser
Component 1: Layout
Test First
Create src/components/__tests__/Layout.test.tsx:
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Layout } from '../Layout'
describe('Layout', () => {
it('renders header with title', () => {
render(<Layout>Content</Layout>)
expect(
screen.getByRole('heading', { name: /stroke lesion segmentation/i })
).toBeInTheDocument()
})
it('renders subtitle', () => {
render(<Layout>Content</Layout>)
expect(screen.getByText(/deepisles segmentation/i)).toBeInTheDocument()
})
it('renders children in main area', () => {
render(
<Layout>
<div data-testid="child">Test Child</div>
</Layout>
)
expect(screen.getByTestId('child')).toBeInTheDocument()
})
it('has accessible landmark structure', () => {
render(<Layout>Content</Layout>)
expect(screen.getByRole('banner')).toBeInTheDocument()
expect(screen.getByRole('main')).toBeInTheDocument()
})
it('applies dark theme styling', () => {
render(<Layout>Content</Layout>)
const container = screen.getByRole('banner').parentElement
expect(container).toHaveClass('bg-gray-950')
})
})
Implementation
Create src/components/Layout.tsx:
import { ReactNode } from 'react'
interface LayoutProps {
children: ReactNode
}
export function Layout({ children }: LayoutProps) {
return (
<div className="min-h-screen bg-gray-950 text-white">
<header className="border-b border-gray-800 py-4">
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold">Stroke Lesion Segmentation</h1>
<p className="text-gray-400 text-sm mt-1">
DeepISLES segmentation on ISLES24 dataset
</p>
</div>
</header>
<main className="container mx-auto px-4 py-6">{children}</main>
</div>
)
}
Verify
npm test -- Layout
# Expected: 5 tests passing
Component 2: MetricsPanel
Test First
Create src/components/__tests__/MetricsPanel.test.tsx:
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MetricsPanel } from '../MetricsPanel'
describe('MetricsPanel', () => {
const defaultMetrics = {
caseId: 'sub-stroke0001',
diceScore: 0.847,
volumeMl: 15.32,
elapsedSeconds: 12.5,
}
it('renders results heading', () => {
render(<MetricsPanel metrics={defaultMetrics} />)
expect(
screen.getByRole('heading', { name: /results/i })
).toBeInTheDocument()
})
it('displays case ID', () => {
render(<MetricsPanel metrics={defaultMetrics} />)
expect(screen.getByText('sub-stroke0001')).toBeInTheDocument()
})
it('displays dice score with 3 decimal places', () => {
render(<MetricsPanel metrics={defaultMetrics} />)
expect(screen.getByText('0.847')).toBeInTheDocument()
})
it('displays volume in mL with 2 decimal places', () => {
render(<MetricsPanel metrics={defaultMetrics} />)
expect(screen.getByText('15.32 mL')).toBeInTheDocument()
})
it('displays elapsed time with 1 decimal place', () => {
render(<MetricsPanel metrics={defaultMetrics} />)
expect(screen.getByText('12.5s')).toBeInTheDocument()
})
it('hides dice score row when null', () => {
render(
<MetricsPanel metrics={{ ...defaultMetrics, diceScore: null }} />
)
expect(screen.queryByText(/dice score/i)).not.toBeInTheDocument()
})
it('hides volume row when null', () => {
render(
<MetricsPanel metrics={{ ...defaultMetrics, volumeMl: null }} />
)
expect(screen.queryByText(/volume/i)).not.toBeInTheDocument()
})
it('applies card styling', () => {
render(<MetricsPanel metrics={defaultMetrics} />)
const panel = screen.getByRole('heading', { name: /results/i }).parentElement
expect(panel).toHaveClass('bg-gray-800', 'rounded-lg')
})
})
Implementation
Create src/components/MetricsPanel.tsx:
interface Metrics {
caseId: string
diceScore: number | null
volumeMl: number | null
elapsedSeconds: number
}
interface MetricsPanelProps {
metrics: Metrics
}
export function MetricsPanel({ metrics }: MetricsPanelProps) {
return (
<div className="bg-gray-800 rounded-lg p-4 space-y-3">
<h3 className="font-medium text-lg">Results</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-400">Case:</span>
<span className="ml-2 font-mono">{metrics.caseId}</span>
</div>
{metrics.diceScore !== null && (
<div>
<span className="text-gray-400">Dice Score:</span>
<span className="ml-2 font-mono text-green-400">
{metrics.diceScore.toFixed(3)}
</span>
</div>
)}
{metrics.volumeMl !== null && (
<div>
<span className="text-gray-400">Volume:</span>
<span className="ml-2 font-mono">
{metrics.volumeMl.toFixed(2)} mL
</span>
</div>
)}
<div>
<span className="text-gray-400">Time:</span>
<span className="ml-2 font-mono">
{metrics.elapsedSeconds.toFixed(1)}s
</span>
</div>
</div>
</div>
)
}
Verify
npm test -- MetricsPanel
# Expected: 8 tests passing
Create Index Export
Create src/components/index.ts:
export { Layout } from './Layout'
export { MetricsPanel } from './MetricsPanel'
Visual Verification
Update src/App.tsx to see components:
import { Layout } from './components/Layout'
import { MetricsPanel } from './components/MetricsPanel'
const mockMetrics = {
caseId: 'sub-stroke0001',
diceScore: 0.847,
volumeMl: 15.32,
elapsedSeconds: 12.5,
}
function App() {
return (
<Layout>
<div className="max-w-md">
<MetricsPanel metrics={mockMetrics} />
</div>
</Layout>
)
}
export default App
Run dev server and verify visually:
npm run dev
# Open http://localhost:5173
Verification Checklist
-
npm test- All 13+ tests pass -
npm run dev- Components render correctly - Header shows "Stroke Lesion Segmentation"
- MetricsPanel shows all metrics with correct formatting
- Dark theme applies correctly
File Structure After This Phase
frontend/src/
βββ components/
β βββ __tests__/
β β βββ Layout.test.tsx
β β βββ MetricsPanel.test.tsx
β βββ Layout.tsx
β βββ MetricsPanel.tsx
β βββ index.ts
βββ mocks/
βββ test/
βββ App.tsx (updated)
βββ ...
Next Phase
Once verification passes, proceed to Spec 37.2: API Layer