Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Releases

## v1.0.0 — Production Release (August 2026)

DevForge reaches v1.0.0 with a complete AI-powered developer platform and
engineering workspace.

### Highlights

- **Project management** — Kanban boards, milestones, roadmaps, task
dependencies, labels and full-text search.
- **GitHub integration** — OAuth connection, repository import/sync,
pull request views, commit history and webhook-driven updates.
- **Real-time collaboration** — Socket.io-powered notifications, team
chat, typing indicators and online presence.
- **Analytics dashboard** — Velocity, health, contributor breakdowns
and repository activity, with weekly developer metrics snapshots.
- **AI repository analyzer** — Architecture, code quality, security and
documentation scores with strengths, risks and recommendations.
- **AI code review** — Severity-classified findings with a review score,
run directly on pull request diffs.
- **AI documentation generator** — Preview-and-approve README and docs
generation; approvals commit directly to GitHub.
- **AI engineering assistant** — Streamed, repo-scoped Q&A grounded in
your indexed repository chunks.
- **DevOps** — Docker Compose full-stack (postgres, api, web, ai, nginx),
Prometheus metrics, CI quality gate with security auditing.

### Test coverage

470 passing tests across four stacks:

| Stack | Tests | Files |
| --- | --- | --- |
| API (Express) | 220 | 17 |
| AI service (FastAPI) | 146 | 21 |
| Web (React) | 89 | 12 |
| Database | 15 | 2 |
| **Total** | **470** | **52** |

### Breaking changes

None — first stable release.

### Acknowledgements

Built incrementally across 13 phases with a CI quality gate enforcing
lint, test, build and structure validation on every push.
2 changes: 1 addition & 1 deletion apps/ai/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
def create_app() -> FastAPI:
app = FastAPI(
title="DevForge AI Service",
version="0.1.0",
version="1.0.0",
description="Repository ingestion, analysis and retrieval for DevForge.",
)
app.include_router(health.router)
Expand Down
2 changes: 1 addition & 1 deletion apps/ai/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "devforge-ai"
version = "0.1.0"
version = "1.0.0"
description = "DevForge AI service — ingestion, embeddings, RAG and the provider gateway"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devforge/api",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "DevForge API service (Express)",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function createApp({ auth = null, modules = null, realtime = null, metric
app.get('/', (_req, res) => {
res.json({
service: 'devforge-api',
version: '0.1.0',
version: '1.0.0',
health: '/api/v1/health',
});
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devforge/web",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "DevForge web application (React + Vite)",
Expand Down
28 changes: 19 additions & 9 deletions apps/web/src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@ import { ForgotPassword } from './pages/forgot-password.jsx';
import { ResetPassword } from './pages/reset-password.jsx';
import { Projects } from './pages/projects.jsx';
import { ProjectDetail } from './pages/project-detail.jsx';
import { TaskDetail } from './pages/task-detail.jsx';
import { Repositories } from './pages/repositories.jsx';
import { RepositoryDetail } from './pages/repository-detail.jsx';
import { Chat } from './pages/chat.jsx';

const Analytics = lazy(() => import('./pages/analytics.jsx'));
const TaskDetail = lazy(() =>
import('./pages/task-detail.jsx').then((m) => ({ default: m.TaskDetail })),
);
const Repositories = lazy(() =>
import('./pages/repositories.jsx').then((m) => ({ default: m.Repositories })),
);
const RepositoryDetail = lazy(() =>
import('./pages/repository-detail.jsx').then((m) => ({ default: m.RepositoryDetail })),
);
const Chat = lazy(() =>
import('./pages/chat.jsx').then((m) => ({ default: m.Chat })),
);
const Analytics = lazy(() =>
import('./pages/analytics.jsx').then((m) => ({ default: m.Analytics })),
);

function LazyFallback() {
return <p className="text-sm text-muted">Loading…</p>;
Expand All @@ -43,10 +53,10 @@ export function App() {
<Route index element={<Dashboard />} />
<Route path="projects" element={<Projects />} />
<Route path="projects/:projectId" element={<ProjectDetail />} />
<Route path="projects/:projectId/tasks/:taskId" element={<TaskDetail />} />
<Route path="repositories" element={<Repositories />} />
<Route path="repositories/:repoId" element={<RepositoryDetail />} />
<Route path="chat" element={<Chat />} />
<Route path="projects/:projectId/tasks/:taskId" element={<Suspense fallback={<LazyFallback />}><TaskDetail /></Suspense>} />
<Route path="repositories" element={<Suspense fallback={<LazyFallback />}><Repositories /></Suspense>} />
<Route path="repositories/:repoId" element={<Suspense fallback={<LazyFallback />}><RepositoryDetail /></Suspense>} />
<Route path="chat" element={<Suspense fallback={<LazyFallback />}><Chat /></Suspense>} />
<Route
path="analytics"
element={
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function Field({ label, children }) {
export function ErrorBanner({ children }) {
if (!children) return null;
return (
<div className="rounded-md border border-red-500/40 px-3 py-2 text-sm text-red-400">
<div role="alert" className="rounded-md border border-red-500/40 px-3 py-2 text-sm text-red-400">
{children}
</div>
);
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/layouts/app-shell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,20 @@ export function AppShell() {

return (
<div className="flex min-h-screen bg-canvas text-ink">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-2 focus:rounded-md focus:bg-accent focus:px-3 focus:py-2 focus:text-sm focus:text-canvas"
>
Skip to main content
</a>
<aside className="flex w-56 shrink-0 flex-col border-r border-line bg-panel">
<div className="flex h-14 items-center gap-2 border-b border-line px-4">
<span className="font-mono text-sm font-semibold tracking-tight text-accent">
DEVFORGE
</span>
</div>

<nav className="flex-1 space-y-1 overflow-y-auto p-3">
<nav aria-label="Main navigation" className="flex-1 space-y-1 overflow-y-auto p-3">
{primaryNav.map((item) => (
<NavLink
key={item.to}
Expand Down Expand Up @@ -142,7 +148,7 @@ export function AppShell() {
</nav>

<div className="border-t border-line p-3 text-[11px] text-muted">
DevForge v0.1.0
DevForge v1.0.0
</div>
</aside>

Expand Down Expand Up @@ -239,14 +245,15 @@ export function AppShell() {
<button
type="button"
onClick={toggleTheme}
aria-label="Toggle theme"
className="rounded-md border border-line px-3 py-1.5 text-xs text-muted hover:bg-panel hover:text-ink"
>
{theme === 'dark' ? 'Light' : 'Dark'}
</button>
</div>
</header>

<main className="flex-1 overflow-y-auto p-6">
<main id="main-content" className="flex-1 overflow-y-auto p-6">
<Outlet />
</main>
</div>
Expand Down
87 changes: 87 additions & 0 deletions apps/web/src/test/a11y.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppShell } from '../layouts/app-shell.jsx';

vi.mock('../services/api.js', () => ({
api: {
listOrganizations: vi.fn().mockResolvedValue([{ id: 'org-1', name: 'Acme Corp' }]),
listNotifications: vi.fn().mockResolvedValue({ items: [], total: 0 }),
},
}));

vi.mock('../services/socket.js', () => ({
connectSocket: vi.fn(),
disconnectSocket: vi.fn(),
onRealtime: vi.fn(() => vi.fn()),
}));

vi.mock('../stores/ui.js', () => ({
useUiStore: vi.fn((selector) => {
const state = { theme: 'dark', toggleTheme: vi.fn() };
return selector(state);
}),
}));

vi.mock('../stores/auth.js', () => ({
useAuthStore: vi.fn((selector) => {
const state = { status: 'authenticated', user: { name: 'Tester' }, logout: vi.fn() };
return selector(state);
}),
}));

vi.mock('../stores/workspace.js', () => ({
useWorkspaceStore: vi.fn((selector) => {
const state = { orgId: 'org-1', selectOrg: vi.fn() };
return selector(state);
}),
}));

vi.mock('../stores/notifications.js', () => ({
useNotificationsStore: Object.assign(
vi.fn(() => ({ items: [], total: 0, unread: 0, loading: false, refresh: vi.fn(), markRead: vi.fn(), markAllRead: vi.fn() })),
{ getState: vi.fn(() => ({ items: [], total: 0, unread: 0 })) },
),
}));

function renderShell() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/']}>
<AppShell />
</MemoryRouter>
</QueryClientProvider>,
);
}

describe('AppShell a11y landmarks', () => {
it('includes a skip-to-content link targeting the main element', () => {
renderShell();
const link = screen.getByRole('link', { name: /skip to main content/i });
expect(link).toHaveAttribute('href', '#main-content');
});

it('labels the primary navigation landmark', () => {
renderShell();
const nav = screen.getByRole('navigation', { name: /main navigation/i });
expect(nav).toBeInTheDocument();
});

it('labels the theme toggle button', () => {
renderShell();
expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument();
});

it('labels the organization selector', async () => {
renderShell();
const select = await screen.findByRole('combobox', { name: /organization/i });
expect(select).toBeInTheDocument();
});

it('labels the notifications button', () => {
renderShell();
expect(screen.getByRole('button', { name: /notifications/i })).toBeInTheDocument();
});
});
5 changes: 3 additions & 2 deletions apps/web/src/test/app-shell.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ describe('AppShell', () => {
const user = userEvent.setup();
renderShell();

const button = screen.getByRole('button', { name: 'Light' });
const button = screen.getByRole('button', { name: /toggle theme/i });
expect(button).toHaveTextContent('Light');
await user.click(button);

expect(document.documentElement.dataset.theme).toBe('light');
expect(window.localStorage.getItem('devforge.theme')).toBe('light');
expect(screen.getByRole('button', { name: 'Dark' })).toBeInTheDocument();
expect(button).toHaveTextContent('Dark');
});
});
4 changes: 4 additions & 0 deletions apps/web/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default defineConfig({
server: {
port: 5173,
},
build: {
cssCodeSplit: true,
sourcemap: false,
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.js'],
Expand Down
Loading
Loading