Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: v2 user-centric todo spec — full Todoist-style app from behaviors

Rewrote spec from user perspective: "users can create tasks with
priorities and due dates" instead of "POST /todos must accept JSON".
Phoenix derives API endpoints, database schema, JOINs, computed
fields (is_overdue, active_task_count, completion_percentage), and
a full web UI from behavioral descriptions.

Architecture target now translates user requirements to implementation:
"users can view X" → GET endpoint, "users can filter by Y" → query
params, "visually highlighted" → UI concern.

Fixed: cmdRegen now loads architecture from config. Architecture stub
fallback produces valid Hono routers. Added trigger/migration guidance.

7 IUs generated: Tasks, Projects, Filtering, Quick Stats, Integration,
Data Integrity, Web Experience.

+2786 -1107
examples/todo-app/data/app.db

This is a binary file and will not be displayed.

examples/todo-app/data/app.db-shm

This is a binary file and will not be displayed.

examples/todo-app/data/app.db-wal

This is a binary file and will not be displayed.

+9 -8
examples/todo-app/package.json
··· 1 1 { 2 2 "name": "todo-app", 3 3 "version": "0.1.0", 4 + "description": "Generated by Phoenix VCS — 1 services", 4 5 "type": "module", 5 - "dependencies": { 6 - "hono": "^4.6.0", 7 - "@hono/node-server": "^1.13.0", 8 - "better-sqlite3": "^11.7.0", 9 - "zod": "^3.24.0" 6 + "scripts": { 7 + "build": "tsc", 8 + "typecheck": "tsc --noEmit", 9 + "test": "vitest run", 10 + "test:watch": "vitest", 11 + "start:todos": "tsc && node dist/generated/todos/server.js", 12 + "start": "tsc && node dist/generated/todos/server.js" 10 13 }, 11 14 "devDependencies": { 12 15 "typescript": "^5.4.0", 13 16 "vitest": "^2.0.0", 14 - "@types/node": "^22.0.0", 15 - "@types/better-sqlite3": "^7.6.0", 16 - "tsx": "^4.0.0" 17 + "@types/node": "^22.0.0" 17 18 } 18 19 }
+54 -36
examples/todo-app/spec/todos.md
··· 1 - # Todo API 1 + # Todoist-style Task Manager 2 2 3 - A REST API for managing todo lists and items, with categories and basic stats. 3 + A personal task manager for organizing work and life. Users manage tasks across projects with priorities, due dates, and completion tracking. The app should be usable from a web browser and integratable from other applications so tasks can surface in Slack, calendar apps, and other tools. 4 4 5 - ## Categories 5 + ## Tasks 6 6 7 - - A category has: id (integer, auto-increment), name (text, required, unique), color (text, default '#888888') 8 - - GET /categories must return all categories as a JSON array 9 - - POST /categories must create a category and return it with 201 10 - - DELETE /categories/:id must delete a category and return 204; if the category has todos, return 400 with an error 11 - - Category name must not be empty and must be at most 50 characters 7 + - A task has a title, an optional description, a priority (urgent, high, normal, low), an optional due date, and a completion status 8 + - Users can create tasks by providing at least a title 9 + - Users can view all their tasks, with the most urgent and overdue tasks shown first 10 + - Users can mark a task as complete or reopen a completed task 11 + - Users can edit a task's title, description, priority, or due date at any time 12 + - Users can delete a task permanently 13 + - Overdue tasks must be visually highlighted so they stand out 14 + - Completed tasks must be visually distinct from active tasks (e.g., strikethrough, dimmed) 12 15 13 - ## Todos 16 + ## Projects 14 17 15 - - A todo has: id (integer, auto-increment), title (text, required), completed (integer 0 or 1, default 0), category_id (integer, nullable foreign key to categories), created_at (timestamp, default now) 16 - - GET /todos must return all todos ordered by created_at descending, each todo must include its category name (as category_name) if it has one 17 - - GET /todos?completed=1 must filter to only completed todos; GET /todos?completed=0 must filter to only incomplete todos 18 - - GET /todos?category_id=N must filter to only todos in that category 19 - - GET /todos/:id must return a single todo with category_name included, or 404 20 - - POST /todos must create a todo with title and optional category_id, return 201 21 - - PATCH /todos/:id must update title, completed, and/or category_id 22 - - DELETE /todos/:id must delete a todo and return 204, or 404 23 - - Title must not be empty and must be at most 200 characters 24 - - If category_id is provided, it must reference an existing category; return 400 otherwise 18 + - A project has a name and a color for visual identification 19 + - Users can create projects to group related tasks 20 + - Users can assign a task to a project, or leave it in a default "Inbox" with no project 21 + - Users can view tasks filtered by project 22 + - Users can delete a project only if it contains no tasks; the system must warn them otherwise 23 + - The project list must show how many active (incomplete) tasks each project has 25 24 26 - ## Stats 25 + ## Filtering and Views 27 26 28 - - GET /todos/stats must return a JSON object with: total (total todo count), completed (completed count), incomplete (incomplete count), by_category (array of {category_name, count} ordered by count descending) 27 + - Users can filter tasks by status: all, active (not completed), or completed 28 + - Users can filter tasks by project 29 + - Users can filter tasks by priority 30 + - Filters must be combinable: a user can view "all urgent tasks in the Work project" for example 31 + - The current filter state must be visible so users know what they're looking at 29 32 30 - ## Web Interface 33 + ## Quick Stats 34 + 35 + - Users must see a summary showing: total tasks, completed tasks, overdue tasks, and completion percentage 36 + - The summary must update immediately when tasks are created, completed, or deleted 37 + 38 + ## Integration 39 + 40 + - The system must expose a programmatic interface so external tools can create, read, update, and delete tasks and projects 41 + - The programmatic interface must use standard conventions so it works with common integration platforms (Zapier, Slack bots, calendar sync) 42 + - Every task and project must have a stable unique identifier that external systems can reference 43 + 44 + ## Web Experience 31 45 32 - - GET / must serve a single-page HTML application with inline CSS and JavaScript 33 - - The page must display a header with the title "Todos" and a stats summary showing total, completed, and incomplete counts 34 - - The page must display a form to create new todos with a text input for title and a dropdown to select a category (populated from GET /categories) 35 - - The page must display all todos as a list, each showing the title, category name as a colored badge, and a checkbox for completed status 36 - - Clicking the checkbox must toggle the todo's completed status via PATCH /todos/:id 37 - - Each todo must have a delete button that removes it via DELETE /todos/:id 38 - - The page must display a category management section where users can add new categories with a name and color picker, and delete empty categories 39 - - The page must include filter buttons: All, Active (incomplete), Completed 40 - - The page must refresh the todo list and stats after every create, update, or delete action 41 - - The design must be clean and modern with a centered layout, max-width 640px, system-ui font, subtle shadows, and a light color scheme 46 + - When users open the app in a browser, they see their task list immediately with no login required 47 + - The main view shows a sidebar with projects (each with its color dot and active task count) and an "Inbox" option for unassigned tasks 48 + - The main area shows the task list for the currently selected project or inbox, with the stats summary at the top 49 + - Each task shows its title, priority as a colored badge (urgent=red, high=orange, normal=blue, low=gray), project name if assigned, due date if set, and a checkbox to toggle completion 50 + - Completed tasks appear with a strikethrough title and dimmed appearance 51 + - Overdue tasks have a red highlight or badge showing they are past due 52 + - There is a prominent "Add task" form at the top of the task list with fields for title, description (collapsible), priority dropdown, project dropdown, and due date picker 53 + - Filter buttons for All / Active / Completed appear above the task list, along with a priority filter dropdown 54 + - Users can delete a task via a small delete button that appears on hover 55 + - The design must be clean, modern, and responsive with a maximum content width of 800px, system-ui font, and a light neutral color scheme with colored accents for priorities and projects 56 + - All interactions (create, complete, edit, delete, filter) must work without page reloads by calling the programmatic interface and updating the display 42 57 43 - ## Error Handling 58 + ## Data Integrity 44 59 45 - - All error responses must be JSON with an "error" field 46 - - Invalid JSON bodies must return 400 47 - - Validation failures must return 400 60 + - Task titles must not be empty and must not exceed 500 characters 61 + - Descriptions must not exceed 5000 characters 62 + - Priority must always be one of: urgent, high, normal, low 63 + - Due dates must be valid dates; the system must reject obviously invalid dates 64 + - Deleting a project must never silently delete its tasks 65 + - The system must never lose data due to concurrent updates; last-write-wins is acceptable for this version
+11
examples/todo-app/src/generated/index.ts
··· 1 + /** 2 + * Phoenix VCS — Generated Service Registry 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + */ 6 + 7 + export * as todos from './todos/index.js'; 8 + 9 + export const services = [ 10 + { name: 'Todos', dir: 'todos', port: 3000, modules: 7 }, 11 + ] as const;
+148
examples/todo-app/src/generated/todos/__tests__/todos.test.ts
··· 1 + /** 2 + * Todos — Generated Tests 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Tests module structure, server health, and Phoenix traceability. 6 + */ 7 + 8 + import { describe, it, expect, afterAll } from 'vitest'; 9 + import { startServer } from '../server.js'; 10 + 11 + import * as dataIntegrity from '../data-integrity.js'; 12 + import * as filteringAndViews from '../filtering-and-views.js'; 13 + import * as integration from '../integration.js'; 14 + import * as projects from '../projects.js'; 15 + import * as quickStats from '../quick-stats.js'; 16 + import * as tasks from '../tasks.js'; 17 + import * as webExperience from '../web-experience.js'; 18 + 19 + describe('Todos modules', () => { 20 + describe('Data Integrity', () => { 21 + it('exports Phoenix traceability metadata', () => { 22 + expect(dataIntegrity._phoenix).toBeDefined(); 23 + expect(dataIntegrity._phoenix.name).toBe('Data Integrity'); 24 + expect(dataIntegrity._phoenix.risk_tier).toBeTruthy(); 25 + }); 26 + 27 + it('has exported functions', () => { 28 + const exports = Object.keys(dataIntegrity).filter(k => k !== '_phoenix'); 29 + expect(exports.length).toBeGreaterThan(0); 30 + }); 31 + }); 32 + 33 + describe('Filtering and Views', () => { 34 + it('exports Phoenix traceability metadata', () => { 35 + expect(filteringAndViews._phoenix).toBeDefined(); 36 + expect(filteringAndViews._phoenix.name).toBe('Filtering and Views'); 37 + expect(filteringAndViews._phoenix.risk_tier).toBeTruthy(); 38 + }); 39 + 40 + it('has exported functions', () => { 41 + const exports = Object.keys(filteringAndViews).filter(k => k !== '_phoenix'); 42 + expect(exports.length).toBeGreaterThan(0); 43 + }); 44 + }); 45 + 46 + describe('Integration', () => { 47 + it('exports Phoenix traceability metadata', () => { 48 + expect(integration._phoenix).toBeDefined(); 49 + expect(integration._phoenix.name).toBe('Integration'); 50 + expect(integration._phoenix.risk_tier).toBeTruthy(); 51 + }); 52 + 53 + it('has exported functions', () => { 54 + const exports = Object.keys(integration).filter(k => k !== '_phoenix'); 55 + expect(exports.length).toBeGreaterThan(0); 56 + }); 57 + }); 58 + 59 + describe('Projects', () => { 60 + it('exports Phoenix traceability metadata', () => { 61 + expect(projects._phoenix).toBeDefined(); 62 + expect(projects._phoenix.name).toBe('Projects'); 63 + expect(projects._phoenix.risk_tier).toBeTruthy(); 64 + }); 65 + 66 + it('has exported functions', () => { 67 + const exports = Object.keys(projects).filter(k => k !== '_phoenix'); 68 + expect(exports.length).toBeGreaterThan(0); 69 + }); 70 + }); 71 + 72 + describe('Quick Stats', () => { 73 + it('exports Phoenix traceability metadata', () => { 74 + expect(quickStats._phoenix).toBeDefined(); 75 + expect(quickStats._phoenix.name).toBe('Quick Stats'); 76 + expect(quickStats._phoenix.risk_tier).toBeTruthy(); 77 + }); 78 + 79 + it('has exported functions', () => { 80 + const exports = Object.keys(quickStats).filter(k => k !== '_phoenix'); 81 + expect(exports.length).toBeGreaterThan(0); 82 + }); 83 + }); 84 + 85 + describe('Tasks', () => { 86 + it('exports Phoenix traceability metadata', () => { 87 + expect(tasks._phoenix).toBeDefined(); 88 + expect(tasks._phoenix.name).toBe('Tasks'); 89 + expect(tasks._phoenix.risk_tier).toBeTruthy(); 90 + }); 91 + 92 + it('has exported functions', () => { 93 + const exports = Object.keys(tasks).filter(k => k !== '_phoenix'); 94 + expect(exports.length).toBeGreaterThan(0); 95 + }); 96 + }); 97 + 98 + describe('Web Experience', () => { 99 + it('exports Phoenix traceability metadata', () => { 100 + expect(webExperience._phoenix).toBeDefined(); 101 + expect(webExperience._phoenix.name).toBe('Web Experience'); 102 + expect(webExperience._phoenix.risk_tier).toBeTruthy(); 103 + }); 104 + 105 + it('has exported functions', () => { 106 + const exports = Object.keys(webExperience).filter(k => k !== '_phoenix'); 107 + expect(exports.length).toBeGreaterThan(0); 108 + }); 109 + }); 110 + 111 + }); 112 + 113 + describe('Todos server', () => { 114 + const instance = startServer(0); // random port 115 + 116 + afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 117 + 118 + it('GET /health returns 200', async () => { 119 + await instance.ready; 120 + const res = await fetch(`http://localhost:${instance.port}/health`); 121 + expect(res.status).toBe(200); 122 + const body = await res.json() as Record<string, unknown>; 123 + expect(body.status).toBe('ok'); 124 + expect(body.service).toBe('Todos'); 125 + }); 126 + 127 + it('GET /metrics returns request counts', async () => { 128 + await instance.ready; 129 + const res = await fetch(`http://localhost:${instance.port}/metrics`); 130 + expect(res.status).toBe(200); 131 + const body = await res.json() as Record<string, unknown>; 132 + expect(typeof body.requests_total).toBe('number'); 133 + }); 134 + 135 + it('GET /modules lists all registered modules', async () => { 136 + await instance.ready; 137 + const res = await fetch(`http://localhost:${instance.port}/modules`); 138 + expect(res.status).toBe(200); 139 + const body = await res.json() as Array<Record<string, unknown>>; 140 + expect(body.length).toBe(7); 141 + }); 142 + 143 + it('GET /unknown returns 404', async () => { 144 + await instance.ready; 145 + const res = await fetch(`http://localhost:${instance.port}/unknown`); 146 + expect(res.status).toBe(404); 147 + }); 148 + });
-77
examples/todo-app/src/generated/todos/categories.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - registerMigration('categories', ` 6 - CREATE TABLE IF NOT EXISTS categories ( 7 - id INTEGER PRIMARY KEY AUTOINCREMENT, 8 - name TEXT NOT NULL UNIQUE, 9 - color TEXT NOT NULL DEFAULT '#888888', 10 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 11 - ) 12 - `); 13 - 14 - const CreateCategorySchema = z.object({ 15 - name: z.string().min(1).max(50), 16 - color: z.string().optional().default('#888888'), 17 - }); 18 - 19 - const router = new Hono(); 20 - 21 - router.get('/', (c) => { 22 - const categories = db.prepare('SELECT * FROM categories ORDER BY name').all(); 23 - return c.json(categories); 24 - }); 25 - 26 - router.post('/', async (c) => { 27 - let body; 28 - try { 29 - body = await c.req.json(); 30 - } catch { 31 - return c.json({ error: 'Invalid JSON' }, 400); 32 - } 33 - 34 - const result = CreateCategorySchema.safeParse(body); 35 - if (!result.success) { 36 - return c.json({ error: result.error.issues[0].message }, 400); 37 - } 38 - 39 - const { name, color } = result.data; 40 - 41 - try { 42 - const info = db.prepare('INSERT INTO categories (name, color) VALUES (?, ?)').run(name, color); 43 - const category = db.prepare('SELECT * FROM categories WHERE id = ?').get(info.lastInsertRowid); 44 - return c.json(category, 201); 45 - } catch (error: unknown) { 46 - if (error && typeof error === 'object' && 'code' in error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 47 - return c.json({ error: 'Category name already exists' }, 400); 48 - } 49 - throw error; 50 - } 51 - }); 52 - 53 - router.delete('/:id', (c) => { 54 - const id = c.req.param('id'); 55 - const existing = db.prepare('SELECT id FROM categories WHERE id = ?').get(id); 56 - if (!existing) { 57 - return c.json({ error: 'Not found' }, 404); 58 - } 59 - 60 - const todosCount = db.prepare('SELECT COUNT(*) as count FROM todos WHERE category_id = ?').get(id) as { count: number }; 61 - if (todosCount.count > 0) { 62 - return c.json({ error: 'Cannot delete category with todos' }, 400); 63 - } 64 - 65 - db.prepare('DELETE FROM categories WHERE id = ?').run(id); 66 - return c.body(null, 204); 67 - }); 68 - 69 - export default router; 70 - 71 - /** @internal Phoenix VCS traceability — do not remove. */ 72 - export const _phoenix = { 73 - iu_id: '643061e5748a224153e0f670e25f0f3b8edb566356e175dbe8555bab2d2adf49', 74 - name: 'Categories', 75 - risk_tier: 'high', 76 - canon_ids: [6 as const], 77 - } as const;
+494
examples/todo-app/src/generated/todos/data-integrity.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register table migrations 6 + registerMigration('projects', ` 7 + CREATE TABLE IF NOT EXISTS projects ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('tasks', ` 16 + CREATE TABLE IF NOT EXISTS tasks ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL CHECK(length(title) > 0 AND length(title) <= 500), 19 + description TEXT NOT NULL DEFAULT '' CHECK(length(description) <= 5000), 20 + priority TEXT NOT NULL CHECK(priority IN ('urgent', 'high', 'normal', 'low')) DEFAULT 'normal', 21 + due_date TEXT, 22 + completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0, 1)), 23 + project_id INTEGER REFERENCES projects(id), 24 + created_at TEXT NOT NULL DEFAULT (datetime('now')), 25 + updated_at TEXT NOT NULL DEFAULT (datetime('now')) 26 + ) 27 + `); 28 + 29 + // Add trigger for updated_at 30 + registerMigration('tasks_updated_at_trigger', ` 31 + CREATE TRIGGER IF NOT EXISTS tasks_updated_at 32 + AFTER UPDATE ON tasks 33 + BEGIN 34 + UPDATE tasks SET updated_at = datetime('now') WHERE id = NEW.id; 35 + END 36 + `); 37 + 38 + const CreateTaskSchema = z.object({ 39 + title: z.string().min(1, 'Title cannot be empty').max(500, 'Title cannot exceed 500 characters'), 40 + description: z.string().max(5000, 'Description cannot exceed 5000 characters').optional().default(''), 41 + priority: z.enum(['urgent', 'high', 'normal', 'low']).default('normal'), 42 + due_date: z.string().refine((date) => { 43 + if (!date) return true; 44 + const parsed = new Date(date); 45 + return !isNaN(parsed.getTime()) && parsed.getFullYear() > 1900 && parsed.getFullYear() < 3000; 46 + }, 'Invalid due date').optional(), 47 + project_id: z.number().int().optional(), 48 + }); 49 + 50 + const UpdateTaskSchema = z.object({ 51 + title: z.string().min(1, 'Title cannot be empty').max(500, 'Title cannot exceed 500 characters').optional(), 52 + description: z.string().max(5000, 'Description cannot exceed 5000 characters').optional(), 53 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 54 + due_date: z.string().refine((date) => { 55 + if (!date) return true; 56 + const parsed = new Date(date); 57 + return !isNaN(parsed.getTime()) && parsed.getFullYear() > 1900 && parsed.getFullYear() < 3000; 58 + }, 'Invalid due date').nullable().optional(), 59 + completed: z.number().int().min(0).max(1).optional(), 60 + project_id: z.number().int().nullable().optional(), 61 + }); 62 + 63 + const CreateProjectSchema = z.object({ 64 + name: z.string().min(1, 'Project name cannot be empty').max(200, 'Project name cannot exceed 200 characters'), 65 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Color must be a valid hex color').default('#3b82f6'), 66 + }); 67 + 68 + const UpdateProjectSchema = z.object({ 69 + name: z.string().min(1, 'Project name cannot be empty').max(200, 'Project name cannot exceed 200 characters').optional(), 70 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Color must be a valid hex color').optional(), 71 + }); 72 + 73 + const router = new Hono(); 74 + 75 + // Task routes 76 + router.get('/tasks', (c) => { 77 + let sql = ` 78 + SELECT 79 + tasks.*, 80 + projects.name as project_name, 81 + projects.color as project_color, 82 + CASE 83 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 84 + ELSE 0 85 + END as overdue 86 + FROM tasks 87 + LEFT JOIN projects ON tasks.project_id = projects.id 88 + `; 89 + 90 + const conditions: string[] = []; 91 + const params: (string | number)[] = []; 92 + 93 + const status = c.req.query('status'); 94 + if (status === 'active') { 95 + conditions.push('tasks.completed = 0'); 96 + } else if (status === 'completed') { 97 + conditions.push('tasks.completed = 1'); 98 + } 99 + 100 + const priority = c.req.query('priority'); 101 + if (priority && ['urgent', 'high', 'normal', 'low'].includes(priority)) { 102 + conditions.push('tasks.priority = ?'); 103 + params.push(priority); 104 + } 105 + 106 + const projectId = c.req.query('project_id'); 107 + if (projectId !== undefined) { 108 + if (projectId === 'null' || projectId === '') { 109 + conditions.push('tasks.project_id IS NULL'); 110 + } else { 111 + conditions.push('tasks.project_id = ?'); 112 + params.push(Number(projectId)); 113 + } 114 + } 115 + 116 + if (conditions.length > 0) { 117 + sql += ' WHERE ' + conditions.join(' AND '); 118 + } 119 + 120 + sql += ` 121 + ORDER BY 122 + tasks.completed ASC, 123 + CASE 124 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 125 + ELSE 1 126 + END ASC, 127 + CASE tasks.priority 128 + WHEN 'urgent' THEN 0 129 + WHEN 'high' THEN 1 130 + WHEN 'normal' THEN 2 131 + WHEN 'low' THEN 3 132 + END ASC, 133 + tasks.created_at DESC 134 + `; 135 + 136 + const tasks = db.prepare(sql).all(...params); 137 + return c.json(tasks); 138 + }); 139 + 140 + router.get('/tasks/:id', (c) => { 141 + const task = db.prepare(` 142 + SELECT 143 + tasks.*, 144 + projects.name as project_name, 145 + projects.color as project_color, 146 + CASE 147 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 148 + ELSE 0 149 + END as overdue 150 + FROM tasks 151 + LEFT JOIN projects ON tasks.project_id = projects.id 152 + WHERE tasks.id = ? 153 + `).get(c.req.param('id')); 154 + 155 + if (!task) return c.json({ error: 'Task not found' }, 404); 156 + return c.json(task); 157 + }); 158 + 159 + router.post('/tasks', async (c) => { 160 + let body; 161 + try { 162 + body = await c.req.json(); 163 + } catch { 164 + return c.json({ error: 'Invalid JSON' }, 400); 165 + } 166 + 167 + const result = CreateTaskSchema.safeParse(body); 168 + if (!result.success) { 169 + return c.json({ error: result.error.issues[0].message }, 400); 170 + } 171 + 172 + const { title, description, priority, due_date, project_id } = result.data; 173 + 174 + // Validate project exists if specified 175 + if (project_id !== undefined) { 176 + const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 177 + if (!project) { 178 + return c.json({ error: 'Project not found' }, 400); 179 + } 180 + } 181 + 182 + try { 183 + const info = db.prepare(` 184 + INSERT INTO tasks (title, description, priority, due_date, project_id) 185 + VALUES (?, ?, ?, ?, ?) 186 + `).run(title, description, priority, due_date || null, project_id || null); 187 + 188 + const task = db.prepare(` 189 + SELECT 190 + tasks.*, 191 + projects.name as project_name, 192 + projects.color as project_color, 193 + CASE 194 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 195 + ELSE 0 196 + END as overdue 197 + FROM tasks 198 + LEFT JOIN projects ON tasks.project_id = projects.id 199 + WHERE tasks.id = ? 200 + `).get(info.lastInsertRowid); 201 + 202 + return c.json(task, 201); 203 + } catch (error) { 204 + if (error instanceof Error && error.message?.includes('CHECK constraint failed')) { 205 + return c.json({ error: 'Data validation failed' }, 400); 206 + } 207 + throw error; 208 + } 209 + }); 210 + 211 + router.patch('/tasks/:id', async (c) => { 212 + const id = c.req.param('id'); 213 + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 214 + if (!existing) return c.json({ error: 'Task not found' }, 404); 215 + 216 + let body; 217 + try { 218 + body = await c.req.json(); 219 + } catch { 220 + return c.json({ error: 'Invalid JSON' }, 400); 221 + } 222 + 223 + const result = UpdateTaskSchema.safeParse(body); 224 + if (!result.success) { 225 + return c.json({ error: result.error.issues[0].message }, 400); 226 + } 227 + 228 + const updates = result.data; 229 + 230 + // Validate project exists if being updated 231 + if (updates.project_id !== undefined && updates.project_id !== null) { 232 + const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 233 + if (!project) { 234 + return c.json({ error: 'Project not found' }, 400); 235 + } 236 + } 237 + 238 + try { 239 + // Update fields individually to handle last-write-wins 240 + if (updates.title !== undefined) { 241 + db.prepare('UPDATE tasks SET title = ? WHERE id = ?').run(updates.title, id); 242 + } 243 + if (updates.description !== undefined) { 244 + db.prepare('UPDATE tasks SET description = ? WHERE id = ?').run(updates.description, id); 245 + } 246 + if (updates.priority !== undefined) { 247 + db.prepare('UPDATE tasks SET priority = ? WHERE id = ?').run(updates.priority, id); 248 + } 249 + if (updates.due_date !== undefined) { 250 + db.prepare('UPDATE tasks SET due_date = ? WHERE id = ?').run(updates.due_date, id); 251 + } 252 + if (updates.completed !== undefined) { 253 + db.prepare('UPDATE tasks SET completed = ? WHERE id = ?').run(updates.completed, id); 254 + } 255 + if (updates.project_id !== undefined) { 256 + db.prepare('UPDATE tasks SET project_id = ? WHERE id = ?').run(updates.project_id, id); 257 + } 258 + 259 + const updated = db.prepare(` 260 + SELECT 261 + tasks.*, 262 + projects.name as project_name, 263 + projects.color as project_color, 264 + CASE 265 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 266 + ELSE 0 267 + END as overdue 268 + FROM tasks 269 + LEFT JOIN projects ON tasks.project_id = projects.id 270 + WHERE tasks.id = ? 271 + `).get(id); 272 + 273 + return c.json(updated); 274 + } catch (error) { 275 + if (error instanceof Error && error.message?.includes('CHECK constraint failed')) { 276 + return c.json({ error: 'Data validation failed' }, 400); 277 + } 278 + throw error; 279 + } 280 + }); 281 + 282 + router.delete('/tasks/:id', (c) => { 283 + const id = c.req.param('id'); 284 + const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 285 + if (!existing) return c.json({ error: 'Task not found' }, 404); 286 + 287 + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 288 + return c.body(null, 204); 289 + }); 290 + 291 + // Project routes 292 + router.get('/projects', (c) => { 293 + const projects = db.prepare(` 294 + SELECT 295 + projects.*, 296 + COUNT(tasks.id) as task_count, 297 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 298 + FROM projects 299 + LEFT JOIN tasks ON projects.id = tasks.project_id 300 + GROUP BY projects.id 301 + ORDER BY projects.name 302 + `).all(); 303 + return c.json(projects); 304 + }); 305 + 306 + router.get('/projects/:id', (c) => { 307 + const project = db.prepare(` 308 + SELECT 309 + projects.*, 310 + COUNT(tasks.id) as task_count, 311 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 312 + FROM projects 313 + LEFT JOIN tasks ON projects.id = tasks.project_id 314 + WHERE projects.id = ? 315 + GROUP BY projects.id 316 + `).get(c.req.param('id')); 317 + 318 + if (!project) return c.json({ error: 'Project not found' }, 404); 319 + return c.json(project); 320 + }); 321 + 322 + router.post('/projects', async (c) => { 323 + let body; 324 + try { 325 + body = await c.req.json(); 326 + } catch { 327 + return c.json({ error: 'Invalid JSON' }, 400); 328 + } 329 + 330 + const result = CreateProjectSchema.safeParse(body); 331 + if (!result.success) { 332 + return c.json({ error: result.error.issues[0].message }, 400); 333 + } 334 + 335 + const { name, color } = result.data; 336 + 337 + try { 338 + const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 339 + const project = db.prepare(` 340 + SELECT 341 + projects.*, 342 + 0 as task_count, 343 + 0 as active_task_count 344 + FROM projects 345 + WHERE id = ? 346 + `).get(info.lastInsertRowid); 347 + return c.json(project, 201); 348 + } catch (error) { 349 + if (error instanceof Error && error.message?.includes('UNIQUE constraint failed')) { 350 + return c.json({ error: 'Project name already exists' }, 400); 351 + } 352 + throw error; 353 + } 354 + }); 355 + 356 + router.patch('/projects/:id', async (c) => { 357 + const id = c.req.param('id'); 358 + const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(id); 359 + if (!existing) return c.json({ error: 'Project not found' }, 404); 360 + 361 + let body; 362 + try { 363 + body = await c.req.json(); 364 + } catch { 365 + return c.json({ error: 'Invalid JSON' }, 400); 366 + } 367 + 368 + const result = UpdateProjectSchema.safeParse(body); 369 + if (!result.success) { 370 + return c.json({ error: result.error.issues[0].message }, 400); 371 + } 372 + 373 + const updates = result.data; 374 + 375 + try { 376 + if (updates.name !== undefined) { 377 + db.prepare('UPDATE projects SET name = ? WHERE id = ?').run(updates.name, id); 378 + } 379 + if (updates.color !== undefined) { 380 + db.prepare('UPDATE projects SET color = ? WHERE id = ?').run(updates.color, id); 381 + } 382 + 383 + const updated = db.prepare(` 384 + SELECT 385 + projects.*, 386 + COUNT(tasks.id) as task_count, 387 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 388 + FROM projects 389 + LEFT JOIN tasks ON projects.id = tasks.project_id 390 + WHERE projects.id = ? 391 + GROUP BY projects.id 392 + `).get(id); 393 + 394 + return c.json(updated); 395 + } catch (error) { 396 + if (error instanceof Error && error.message?.includes('UNIQUE constraint failed')) { 397 + return c.json({ error: 'Project name already exists' }, 400); 398 + } 399 + throw error; 400 + } 401 + }); 402 + 403 + router.delete('/projects/:id', (c) => { 404 + const id = c.req.param('id'); 405 + const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 406 + if (!existing) return c.json({ error: 'Project not found' }, 404); 407 + 408 + // Check for dependent tasks - prevent cascade deletion 409 + const dependentTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 410 + if (dependentTasks.count > 0) { 411 + return c.json({ 412 + error: `Cannot delete project with ${dependentTasks.count} associated tasks. Please reassign or delete tasks first.` 413 + }, 400); 414 + } 415 + 416 + db.prepare('DELETE FROM projects WHERE id = ?').run(id); 417 + return c.body(null, 204); 418 + }); 419 + 420 + // Data integrity validation endpoint 421 + router.get('/validate', (c) => { 422 + const issues: string[] = []; 423 + 424 + // Check for orphaned tasks (referencing non-existent projects) 425 + const orphanedTasks = db.prepare(` 426 + SELECT COUNT(*) as count 427 + FROM tasks 428 + WHERE project_id IS NOT NULL 429 + AND project_id NOT IN (SELECT id FROM projects) 430 + `).get() as { count: number }; 431 + 432 + if (orphanedTasks.count > 0) { 433 + issues.push(`${orphanedTasks.count} tasks reference non-existent projects`); 434 + } 435 + 436 + // Check for invalid due dates 437 + const invalidDates = db.prepare(` 438 + SELECT COUNT(*) as count 439 + FROM tasks 440 + WHERE due_date IS NOT NULL 441 + AND (due_date = '' OR date(due_date) IS NULL) 442 + `).get() as { count: number }; 443 + 444 + if (invalidDates.count > 0) { 445 + issues.push(`${invalidDates.count} tasks have invalid due dates`); 446 + } 447 + 448 + // Check for constraint violations that might have slipped through 449 + const invalidTitles = db.prepare(` 450 + SELECT COUNT(*) as count 451 + FROM tasks 452 + WHERE title = '' OR length(title) > 500 453 + `).get() as { count: number }; 454 + 455 + if (invalidTitles.count > 0) { 456 + issues.push(`${invalidTitles.count} tasks have invalid titles`); 457 + } 458 + 459 + const invalidDescriptions = db.prepare(` 460 + SELECT COUNT(*) as count 461 + FROM tasks 462 + WHERE length(description) > 5000 463 + `).get() as { count: number }; 464 + 465 + if (invalidDescriptions.count > 0) { 466 + issues.push(`${invalidDescriptions.count} tasks have descriptions exceeding 5000 characters`); 467 + } 468 + 469 + const invalidPriorities = db.prepare(` 470 + SELECT COUNT(*) as count 471 + FROM tasks 472 + WHERE priority NOT IN ('urgent', 'high', 'normal', 'low') 473 + `).get() as { count: number }; 474 + 475 + if (invalidPriorities.count > 0) { 476 + issues.push(`${invalidPriorities.count} tasks have invalid priority values`); 477 + } 478 + 479 + return c.json({ 480 + valid: issues.length === 0, 481 + issues: issues, 482 + checked_at: new Date().toISOString() 483 + }); 484 + }); 485 + 486 + export default router; 487 + 488 + /** @internal Phoenix VCS traceability — do not remove. */ 489 + export const _phoenix = { 490 + iu_id: '1f677d4ba5f46a3cd75931c51f4bdc76ac0da22a981004342a40d675ad84749b', 491 + name: 'Data Integrity', 492 + risk_tier: 'high', 493 + canon_ids: [9 as const], 494 + } as const;
-94
examples/todo-app/src/generated/todos/error-handling.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - const router = new Hono(); 6 - 7 - // Global error handler middleware 8 - router.onError((err, c) => { 9 - console.error('Unhandled error:', err); 10 - return c.json({ error: 'Internal server error' }, 500); 11 - }); 12 - 13 - // Middleware to handle invalid JSON 14 - router.use('*', async (c, next) => { 15 - if (c.req.method === 'POST' || c.req.method === 'PATCH' || c.req.method === 'PUT') { 16 - const contentType = c.req.header('content-type'); 17 - if (contentType && contentType.includes('application/json')) { 18 - try { 19 - // Pre-parse JSON to catch syntax errors 20 - const body = await c.req.text(); 21 - if (body.trim()) { 22 - JSON.parse(body); 23 - } 24 - } catch (error) { 25 - return c.json({ error: 'Invalid JSON body' }, 400); 26 - } 27 - } 28 - } 29 - await next(); 30 - }); 31 - 32 - // Test endpoint to demonstrate error handling 33 - router.get('/test-errors', (c) => { 34 - const type = c.req.query('type'); 35 - 36 - switch (type) { 37 - case 'validation': 38 - return c.json({ error: 'Validation failed: title is required' }, 400); 39 - case 'not-found': 40 - return c.json({ error: 'Resource not found' }, 404); 41 - case 'invalid-json': 42 - return c.json({ error: 'Invalid JSON body' }, 400); 43 - case 'server-error': 44 - throw new Error('Simulated server error'); 45 - default: 46 - return c.json({ 47 - message: 'Error handling test endpoint', 48 - available_types: ['validation', 'not-found', 'invalid-json', 'server-error'] 49 - }); 50 - } 51 - }); 52 - 53 - // Test endpoint for JSON validation 54 - router.post('/test-json', async (c) => { 55 - try { 56 - const body = await c.req.json(); 57 - return c.json({ message: 'Valid JSON received', data: body }); 58 - } catch (error) { 59 - return c.json({ error: 'Invalid JSON body' }, 400); 60 - } 61 - }); 62 - 63 - // Test endpoint for Zod validation 64 - const TestSchema = z.object({ 65 - name: z.string().min(1, 'Name is required'), 66 - email: z.string().email('Invalid email format'), 67 - age: z.number().int().min(0, 'Age must be non-negative'), 68 - }); 69 - 70 - router.post('/test-validation', async (c) => { 71 - try { 72 - const body = await c.req.json(); 73 - const result = TestSchema.safeParse(body); 74 - 75 - if (!result.success) { 76 - const firstError = result.error.issues[0]; 77 - return c.json({ error: firstError.message }, 400); 78 - } 79 - 80 - return c.json({ message: 'Validation passed', data: result.data }); 81 - } catch (error) { 82 - return c.json({ error: 'Invalid JSON body' }, 400); 83 - } 84 - }); 85 - 86 - export default router; 87 - 88 - /** @internal Phoenix VCS traceability — do not remove. */ 89 - export const _phoenix = { 90 - iu_id: '1ada78d27b09eccf1ebece746bdd645cf9dccc6e35efdbdeb0d23fa194400152', 91 - name: 'Error Handling', 92 - risk_tier: 'low', 93 - canon_ids: [3 as const], 94 - } as const;
+182
examples/todo-app/src/generated/todos/filtering-and-views.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register migrations for tables this module touches 6 + registerMigration('projects', ` 7 + CREATE TABLE IF NOT EXISTS projects ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('tasks', ` 16 + CREATE TABLE IF NOT EXISTS tasks ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL, 19 + description TEXT DEFAULT '', 20 + priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 + due_date TEXT, 22 + completed INTEGER NOT NULL DEFAULT 0, 23 + project_id INTEGER REFERENCES projects(id), 24 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 25 + ) 26 + `); 27 + 28 + const router = new Hono(); 29 + 30 + // Get filtered tasks with combined filters 31 + router.get('/tasks', (c) => { 32 + let sql = ` 33 + SELECT 34 + tasks.*, 35 + projects.name as project_name, 36 + projects.color as project_color, 37 + CASE 38 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 39 + THEN 1 40 + ELSE 0 41 + END as is_overdue 42 + FROM tasks 43 + LEFT JOIN projects ON tasks.project_id = projects.id 44 + `; 45 + 46 + const conditions: string[] = []; 47 + const params: (string | number)[] = []; 48 + 49 + // Filter by completion status 50 + const status = c.req.query('status'); 51 + if (status === 'active') { 52 + conditions.push('tasks.completed = 0'); 53 + } else if (status === 'completed') { 54 + conditions.push('tasks.completed = 1'); 55 + } 56 + 57 + // Filter by project 58 + const projectId = c.req.query('project_id'); 59 + if (projectId !== undefined) { 60 + if (projectId === 'inbox') { 61 + conditions.push('tasks.project_id IS NULL'); 62 + } else { 63 + conditions.push('tasks.project_id = ?'); 64 + params.push(Number(projectId)); 65 + } 66 + } 67 + 68 + // Filter by priority 69 + const priority = c.req.query('priority'); 70 + if (priority && ['urgent', 'high', 'normal', 'low'].includes(priority)) { 71 + conditions.push('tasks.priority = ?'); 72 + params.push(priority); 73 + } 74 + 75 + if (conditions.length > 0) { 76 + sql += ' WHERE ' + conditions.join(' AND '); 77 + } 78 + 79 + // Sort by urgency and overdue status first 80 + sql += ` ORDER BY 81 + is_overdue DESC, 82 + CASE tasks.priority 83 + WHEN 'urgent' THEN 0 84 + WHEN 'high' THEN 1 85 + WHEN 'normal' THEN 2 86 + WHEN 'low' THEN 3 87 + END, 88 + tasks.created_at DESC 89 + `; 90 + 91 + const tasks = db.prepare(sql).all(...params); 92 + 93 + // Build current filter state 94 + const filterState = { 95 + status: status || 'all', 96 + project_id: projectId || null, 97 + priority: priority || null, 98 + active_filters: [] as string[] 99 + }; 100 + 101 + if (status && status !== 'all') { 102 + filterState.active_filters.push(`Status: ${status}`); 103 + } 104 + if (projectId) { 105 + if (projectId === 'inbox') { 106 + filterState.active_filters.push('Project: Inbox'); 107 + } else { 108 + const project = db.prepare('SELECT name FROM projects WHERE id = ?').get(projectId) as { name: string } | undefined; 109 + if (project) { 110 + filterState.active_filters.push(`Project: ${project.name}`); 111 + } 112 + } 113 + } 114 + if (priority) { 115 + filterState.active_filters.push(`Priority: ${priority}`); 116 + } 117 + 118 + return c.json({ 119 + tasks, 120 + filter_state: filterState 121 + }); 122 + }); 123 + 124 + // Get filter options for dropdowns 125 + router.get('/filter-options', (c) => { 126 + const projects = db.prepare('SELECT id, name, color FROM projects ORDER BY name').all(); 127 + const priorities = ['urgent', 'high', 'normal', 'low']; 128 + const statuses = [ 129 + { value: 'all', label: 'All' }, 130 + { value: 'active', label: 'Active' }, 131 + { value: 'completed', label: 'Completed' } 132 + ]; 133 + 134 + return c.json({ 135 + projects: [ 136 + { id: 'inbox', name: 'Inbox', color: '#6b7280' }, 137 + ...projects 138 + ], 139 + priorities, 140 + statuses 141 + }); 142 + }); 143 + 144 + // Get tasks count by filter combinations (for stats) 145 + router.get('/filter-stats', (c) => { 146 + const stats = { 147 + total: db.prepare('SELECT COUNT(*) as count FROM tasks').get() as { count: number }, 148 + active: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 0').get() as { count: number }, 149 + completed: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 1').get() as { count: number }, 150 + overdue: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE due_date < date("now") AND completed = 0').get() as { count: number }, 151 + by_priority: db.prepare(` 152 + SELECT 153 + priority, 154 + COUNT(*) as count, 155 + COUNT(CASE WHEN completed = 0 THEN 1 END) as active_count 156 + FROM tasks 157 + GROUP BY priority 158 + `).all(), 159 + by_project: db.prepare(` 160 + SELECT 161 + COALESCE(projects.name, 'Inbox') as project_name, 162 + COALESCE(projects.id, 'inbox') as project_id, 163 + COUNT(*) as count, 164 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_count 165 + FROM tasks 166 + LEFT JOIN projects ON tasks.project_id = projects.id 167 + GROUP BY tasks.project_id, projects.name 168 + `).all() 169 + }; 170 + 171 + return c.json(stats); 172 + }); 173 + 174 + export default router; 175 + 176 + /** @internal Phoenix VCS traceability — do not remove. */ 177 + export const _phoenix = { 178 + iu_id: 'c986c6a7885993ce90d626af61ecc90d5de2801eac95c0ff99b368e0e90e8bcc', 179 + name: 'Filtering and Views', 180 + risk_tier: 'low', 181 + canon_ids: [2 as const], 182 + } as const;
+14
examples/todo-app/src/generated/todos/index.ts
··· 1 + /** 2 + * Todos 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Todos modules. 6 + */ 7 + 8 + export * as dataIntegrity from './data-integrity.js'; 9 + export * as filteringAndViews from './filtering-and-views.js'; 10 + export * as integration from './integration.js'; 11 + export * as projects from './projects.js'; 12 + export * as quickStats from './quick-stats.js'; 13 + export * as tasks from './tasks.js'; 14 + export * as webExperience from './web-experience.js';
+356
examples/todo-app/src/generated/todos/integration.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register migrations for both tasks and projects tables 6 + registerMigration('projects', ` 7 + CREATE TABLE IF NOT EXISTS projects ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('tasks', ` 16 + CREATE TABLE IF NOT EXISTS tasks ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL, 19 + description TEXT NOT NULL DEFAULT '', 20 + priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 + due_date TEXT, 22 + completed INTEGER NOT NULL DEFAULT 0, 23 + project_id INTEGER REFERENCES projects(id), 24 + created_at TEXT NOT NULL DEFAULT (datetime('now')), 25 + updated_at TEXT NOT NULL DEFAULT (datetime('now')) 26 + ) 27 + `); 28 + 29 + const CreateTaskSchema = z.object({ 30 + title: z.string().min(1).max(200), 31 + description: z.string().optional().default(''), 32 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional().default('normal'), 33 + due_date: z.string().nullable().optional(), 34 + project_id: z.number().int().nullable().optional(), 35 + }); 36 + 37 + const UpdateTaskSchema = z.object({ 38 + title: z.string().min(1).max(200).optional(), 39 + description: z.string().optional(), 40 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 41 + due_date: z.string().nullable().optional(), 42 + completed: z.number().int().min(0).max(1).optional(), 43 + project_id: z.number().int().nullable().optional(), 44 + }); 45 + 46 + const CreateProjectSchema = z.object({ 47 + name: z.string().min(1).max(100), 48 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().default('#3b82f6'), 49 + }); 50 + 51 + const UpdateProjectSchema = z.object({ 52 + name: z.string().min(1).max(100).optional(), 53 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(), 54 + }); 55 + 56 + const router = new Hono(); 57 + 58 + // Tasks endpoints 59 + router.get('/tasks', (c) => { 60 + let sql = ` 61 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 62 + FROM tasks 63 + LEFT JOIN projects ON tasks.project_id = projects.id 64 + `; 65 + const conditions: string[] = []; 66 + const params: (string | number)[] = []; 67 + 68 + const status = c.req.query('status'); 69 + if (status === 'active') { 70 + conditions.push('tasks.completed = 0'); 71 + } else if (status === 'completed') { 72 + conditions.push('tasks.completed = 1'); 73 + } 74 + 75 + const priority = c.req.query('priority'); 76 + if (priority) { 77 + conditions.push('tasks.priority = ?'); 78 + params.push(priority); 79 + } 80 + 81 + const projectId = c.req.query('project_id'); 82 + if (projectId) { 83 + conditions.push('tasks.project_id = ?'); 84 + params.push(Number(projectId)); 85 + } 86 + 87 + if (conditions.length > 0) { 88 + sql += ' WHERE ' + conditions.join(' AND '); 89 + } 90 + 91 + sql += ` ORDER BY 92 + tasks.completed ASC, 93 + CASE tasks.priority 94 + WHEN 'urgent' THEN 0 95 + WHEN 'high' THEN 1 96 + WHEN 'normal' THEN 2 97 + WHEN 'low' THEN 3 98 + END, 99 + CASE 100 + WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 101 + ELSE 1 102 + END, 103 + tasks.due_date ASC NULLS LAST, 104 + tasks.created_at DESC 105 + `; 106 + 107 + return c.json(db.prepare(sql).all(...params)); 108 + }); 109 + 110 + router.get('/tasks/:id', (c) => { 111 + const task = db.prepare(` 112 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 113 + FROM tasks 114 + LEFT JOIN projects ON tasks.project_id = projects.id 115 + WHERE tasks.id = ? 116 + `).get(c.req.param('id')); 117 + 118 + if (!task) return c.json({ error: 'Task not found' }, 404); 119 + return c.json(task); 120 + }); 121 + 122 + router.post('/tasks', async (c) => { 123 + let body; 124 + try { 125 + body = await c.req.json(); 126 + } catch { 127 + return c.json({ error: 'Invalid JSON' }, 400); 128 + } 129 + 130 + const result = CreateTaskSchema.safeParse(body); 131 + if (!result.success) { 132 + return c.json({ error: result.error.issues[0].message }, 400); 133 + } 134 + 135 + const { title, description, priority, due_date, project_id } = result.data; 136 + 137 + if (project_id !== null && project_id !== undefined) { 138 + const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 139 + if (!project) { 140 + return c.json({ error: 'Project not found' }, 400); 141 + } 142 + } 143 + 144 + const info = db.prepare(` 145 + INSERT INTO tasks (title, description, priority, due_date, project_id) 146 + VALUES (?, ?, ?, ?, ?) 147 + `).run(title, description, priority, due_date, project_id); 148 + 149 + const task = db.prepare(` 150 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 151 + FROM tasks 152 + LEFT JOIN projects ON tasks.project_id = projects.id 153 + WHERE tasks.id = ? 154 + `).get(info.lastInsertRowid); 155 + 156 + return c.json(task, 201); 157 + }); 158 + 159 + router.patch('/tasks/:id', async (c) => { 160 + const id = c.req.param('id'); 161 + const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 162 + if (!existing) return c.json({ error: 'Task not found' }, 404); 163 + 164 + let body; 165 + try { 166 + body = await c.req.json(); 167 + } catch { 168 + return c.json({ error: 'Invalid JSON' }, 400); 169 + } 170 + 171 + const result = UpdateTaskSchema.safeParse(body); 172 + if (!result.success) { 173 + return c.json({ error: result.error.issues[0].message }, 400); 174 + } 175 + 176 + const updates = result.data; 177 + 178 + if (updates.project_id !== undefined && updates.project_id !== null) { 179 + const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 180 + if (!project) { 181 + return c.json({ error: 'Project not found' }, 400); 182 + } 183 + } 184 + 185 + if (updates.title !== undefined) { 186 + db.prepare('UPDATE tasks SET title = ?, updated_at = datetime("now") WHERE id = ?').run(updates.title, id); 187 + } 188 + if (updates.description !== undefined) { 189 + db.prepare('UPDATE tasks SET description = ?, updated_at = datetime("now") WHERE id = ?').run(updates.description, id); 190 + } 191 + if (updates.priority !== undefined) { 192 + db.prepare('UPDATE tasks SET priority = ?, updated_at = datetime("now") WHERE id = ?').run(updates.priority, id); 193 + } 194 + if (updates.due_date !== undefined) { 195 + db.prepare('UPDATE tasks SET due_date = ?, updated_at = datetime("now") WHERE id = ?').run(updates.due_date, id); 196 + } 197 + if (updates.completed !== undefined) { 198 + db.prepare('UPDATE tasks SET completed = ?, updated_at = datetime("now") WHERE id = ?').run(updates.completed, id); 199 + } 200 + if (updates.project_id !== undefined) { 201 + db.prepare('UPDATE tasks SET project_id = ?, updated_at = datetime("now") WHERE id = ?').run(updates.project_id, id); 202 + } 203 + 204 + const updated = db.prepare(` 205 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 206 + FROM tasks 207 + LEFT JOIN projects ON tasks.project_id = projects.id 208 + WHERE tasks.id = ? 209 + `).get(id); 210 + 211 + return c.json(updated); 212 + }); 213 + 214 + router.delete('/tasks/:id', (c) => { 215 + const id = c.req.param('id'); 216 + const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 217 + if (!existing) return c.json({ error: 'Task not found' }, 404); 218 + 219 + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 220 + return c.body(null, 204); 221 + }); 222 + 223 + // Projects endpoints 224 + router.get('/projects', (c) => { 225 + const projects = db.prepare(` 226 + SELECT projects.*, 227 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count, 228 + COUNT(tasks.id) as total_task_count 229 + FROM projects 230 + LEFT JOIN tasks ON projects.id = tasks.project_id 231 + GROUP BY projects.id 232 + ORDER BY projects.created_at DESC 233 + `).all(); 234 + 235 + return c.json(projects); 236 + }); 237 + 238 + router.get('/projects/:id', (c) => { 239 + const project = db.prepare(` 240 + SELECT projects.*, 241 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count, 242 + COUNT(tasks.id) as total_task_count 243 + FROM projects 244 + LEFT JOIN tasks ON projects.id = tasks.project_id 245 + WHERE projects.id = ? 246 + GROUP BY projects.id 247 + `).get(c.req.param('id')); 248 + 249 + if (!project) return c.json({ error: 'Project not found' }, 404); 250 + return c.json(project); 251 + }); 252 + 253 + router.post('/projects', async (c) => { 254 + let body; 255 + try { 256 + body = await c.req.json(); 257 + } catch { 258 + return c.json({ error: 'Invalid JSON' }, 400); 259 + } 260 + 261 + const result = CreateProjectSchema.safeParse(body); 262 + if (!result.success) { 263 + return c.json({ error: result.error.issues[0].message }, 400); 264 + } 265 + 266 + const { name, color } = result.data; 267 + 268 + try { 269 + const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 270 + const project = db.prepare(` 271 + SELECT projects.*, 272 + 0 as active_task_count, 273 + 0 as total_task_count 274 + FROM projects 275 + WHERE projects.id = ? 276 + `).get(info.lastInsertRowid); 277 + 278 + return c.json(project, 201); 279 + } catch (error: any) { 280 + if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 281 + return c.json({ error: 'Project name already exists' }, 400); 282 + } 283 + throw error; 284 + } 285 + }); 286 + 287 + router.patch('/projects/:id', async (c) => { 288 + const id = c.req.param('id'); 289 + const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 290 + if (!existing) return c.json({ error: 'Project not found' }, 404); 291 + 292 + let body; 293 + try { 294 + body = await c.req.json(); 295 + } catch { 296 + return c.json({ error: 'Invalid JSON' }, 400); 297 + } 298 + 299 + const result = UpdateProjectSchema.safeParse(body); 300 + if (!result.success) { 301 + return c.json({ error: result.error.issues[0].message }, 400); 302 + } 303 + 304 + const updates = result.data; 305 + 306 + try { 307 + if (updates.name !== undefined) { 308 + db.prepare('UPDATE projects SET name = ? WHERE id = ?').run(updates.name, id); 309 + } 310 + if (updates.color !== undefined) { 311 + db.prepare('UPDATE projects SET color = ? WHERE id = ?').run(updates.color, id); 312 + } 313 + 314 + const updated = db.prepare(` 315 + SELECT projects.*, 316 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count, 317 + COUNT(tasks.id) as total_task_count 318 + FROM projects 319 + LEFT JOIN tasks ON projects.id = tasks.project_id 320 + WHERE projects.id = ? 321 + GROUP BY projects.id 322 + `).get(id); 323 + 324 + return c.json(updated); 325 + } catch (error: any) { 326 + if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 327 + return c.json({ error: 'Project name already exists' }, 400); 328 + } 329 + throw error; 330 + } 331 + }); 332 + 333 + router.delete('/projects/:id', (c) => { 334 + const id = c.req.param('id'); 335 + const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 336 + if (!existing) return c.json({ error: 'Project not found' }, 404); 337 + 338 + // Check for dependent tasks 339 + const taskCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 340 + if (taskCount.count > 0) { 341 + return c.json({ error: 'Cannot delete project with existing tasks' }, 400); 342 + } 343 + 344 + db.prepare('DELETE FROM projects WHERE id = ?').run(id); 345 + return c.body(null, 204); 346 + }); 347 + 348 + export default router; 349 + 350 + /** @internal Phoenix VCS traceability — do not remove. */ 351 + export const _phoenix = { 352 + iu_id: '7ee19d155ffb4b7ff0346e313207867d19efacb2af2bfcb3dce82a7f2adfd73f', 353 + name: 'Integration', 354 + risk_tier: 'low', 355 + canon_ids: [3 as const], 356 + } as const;
+196
examples/todo-app/src/generated/todos/projects.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register table migrations 6 + registerMigration('projects', ` 7 + CREATE TABLE IF NOT EXISTS projects ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('tasks', ` 16 + CREATE TABLE IF NOT EXISTS tasks ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL, 19 + description TEXT DEFAULT '', 20 + priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 + due_date TEXT, 22 + completed INTEGER NOT NULL DEFAULT 0, 23 + project_id INTEGER REFERENCES projects(id), 24 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 25 + ) 26 + `); 27 + 28 + const CreateProjectSchema = z.object({ 29 + name: z.string().min(1).max(200), 30 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().default('#3b82f6'), 31 + }); 32 + 33 + const UpdateProjectSchema = z.object({ 34 + name: z.string().min(1).max(200).optional(), 35 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(), 36 + }); 37 + 38 + const router = new Hono(); 39 + 40 + // List all projects with active task counts 41 + router.get('/', (c) => { 42 + const projects = db.prepare(` 43 + SELECT 44 + projects.*, 45 + COALESCE(task_counts.active_count, 0) as active_task_count 46 + FROM projects 47 + LEFT JOIN ( 48 + SELECT 49 + project_id, 50 + COUNT(*) as active_count 51 + FROM tasks 52 + WHERE completed = 0 53 + GROUP BY project_id 54 + ) task_counts ON projects.id = task_counts.project_id 55 + ORDER BY projects.created_at DESC 56 + `).all(); 57 + return c.json(projects); 58 + }); 59 + 60 + // Get single project 61 + router.get('/:id', (c) => { 62 + const project = db.prepare(` 63 + SELECT 64 + projects.*, 65 + COALESCE(task_counts.active_count, 0) as active_task_count 66 + FROM projects 67 + LEFT JOIN ( 68 + SELECT 69 + project_id, 70 + COUNT(*) as active_count 71 + FROM tasks 72 + WHERE completed = 0 73 + GROUP BY project_id 74 + ) task_counts ON projects.id = task_counts.project_id 75 + WHERE projects.id = ? 76 + `).get(c.req.param('id')); 77 + 78 + if (!project) return c.json({ error: 'Project not found' }, 404); 79 + return c.json(project); 80 + }); 81 + 82 + // Create project 83 + router.post('/', async (c) => { 84 + let body; 85 + try { 86 + body = await c.req.json(); 87 + } catch { 88 + return c.json({ error: 'Invalid JSON' }, 400); 89 + } 90 + 91 + const result = CreateProjectSchema.safeParse(body); 92 + if (!result.success) { 93 + return c.json({ error: result.error.issues[0].message }, 400); 94 + } 95 + 96 + const { name, color } = result.data; 97 + 98 + try { 99 + const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 100 + const project = db.prepare(` 101 + SELECT 102 + projects.*, 103 + 0 as active_task_count 104 + FROM projects 105 + WHERE projects.id = ? 106 + `).get(info.lastInsertRowid); 107 + return c.json(project, 201); 108 + } catch (error: any) { 109 + if (error && typeof error === 'object' && 'code' in error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 110 + return c.json({ error: 'Project name already exists' }, 400); 111 + } 112 + throw error; 113 + } 114 + }); 115 + 116 + // Update project 117 + router.patch('/:id', async (c) => { 118 + const id = c.req.param('id'); 119 + const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(id); 120 + if (!existing) return c.json({ error: 'Project not found' }, 404); 121 + 122 + let body; 123 + try { 124 + body = await c.req.json(); 125 + } catch { 126 + return c.json({ error: 'Invalid JSON' }, 400); 127 + } 128 + 129 + const result = UpdateProjectSchema.safeParse(body); 130 + if (!result.success) { 131 + return c.json({ error: result.error.issues[0].message }, 400); 132 + } 133 + 134 + const updates = result.data; 135 + 136 + try { 137 + if (updates.name !== undefined) { 138 + db.prepare('UPDATE projects SET name = ? WHERE id = ?').run(updates.name, id); 139 + } 140 + if (updates.color !== undefined) { 141 + db.prepare('UPDATE projects SET color = ? WHERE id = ?').run(updates.color, id); 142 + } 143 + 144 + const updated = db.prepare(` 145 + SELECT 146 + projects.*, 147 + COALESCE(task_counts.active_count, 0) as active_task_count 148 + FROM projects 149 + LEFT JOIN ( 150 + SELECT 151 + project_id, 152 + COUNT(*) as active_count 153 + FROM tasks 154 + WHERE completed = 0 155 + GROUP BY project_id 156 + ) task_counts ON projects.id = task_counts.project_id 157 + WHERE projects.id = ? 158 + `).get(id); 159 + 160 + return c.json(updated); 161 + } catch (error: any) { 162 + if (error && typeof error === 'object' && 'code' in error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 163 + return c.json({ error: 'Project name already exists' }, 400); 164 + } 165 + throw error; 166 + } 167 + }); 168 + 169 + // Delete project (with cascade protection) 170 + router.delete('/:id', (c) => { 171 + const id = c.req.param('id'); 172 + const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(id); 173 + if (!existing) return c.json({ error: 'Project not found' }, 404); 174 + 175 + // Check for tasks in this project 176 + const taskCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 177 + if (taskCount.count > 0) { 178 + return c.json({ 179 + error: 'Cannot delete project that contains tasks', 180 + task_count: taskCount.count 181 + }, 400); 182 + } 183 + 184 + db.prepare('DELETE FROM projects WHERE id = ?').run(id); 185 + return c.body(null, 204); 186 + }); 187 + 188 + export default router; 189 + 190 + /** @internal Phoenix VCS traceability — do not remove. */ 191 + export const _phoenix = { 192 + iu_id: '684e98680b126a8a1535a88875ffb4157cfc3bc1881f7a6b34c2fdae1830e9b1', 193 + name: 'Projects', 194 + risk_tier: 'low', 195 + canon_ids: [3 as const], 196 + } as const;
+62
examples/todo-app/src/generated/todos/quick-stats.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register migrations for tables this module reads from 6 + registerMigration('projects', ` 7 + CREATE TABLE IF NOT EXISTS projects ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('tasks', ` 16 + CREATE TABLE IF NOT EXISTS tasks ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL, 19 + description TEXT NOT NULL DEFAULT '', 20 + priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 + due_date TEXT, 22 + completed INTEGER NOT NULL DEFAULT 0, 23 + project_id INTEGER REFERENCES projects(id), 24 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 25 + ) 26 + `); 27 + 28 + const router = new Hono(); 29 + 30 + // Get quick stats summary 31 + router.get('/', (c) => { 32 + // Total tasks 33 + const totalTasks = db.prepare('SELECT COUNT(*) as count FROM tasks').get() as { count: number }; 34 + 35 + // Completed tasks 36 + const completedTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 1').get() as { count: number }; 37 + 38 + // Overdue tasks (due date is past and not completed) 39 + const overdueTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE due_date < date("now") AND completed = 0').get() as { count: number }; 40 + 41 + // Calculate completion percentage 42 + const completionPercentage = totalTasks.count > 0 43 + ? Math.round((completedTasks.count / totalTasks.count) * 100) 44 + : 0; 45 + 46 + return c.json({ 47 + total_tasks: totalTasks.count, 48 + completed_tasks: completedTasks.count, 49 + overdue_tasks: overdueTasks.count, 50 + completion_percentage: completionPercentage 51 + }); 52 + }); 53 + 54 + export default router; 55 + 56 + /** @internal Phoenix VCS traceability — do not remove. */ 57 + export const _phoenix = { 58 + iu_id: 'e971f2b7f67c9ac5f5b54f0baf9d19f5e50593b2fc1e9f9c93fb01f6029e712e', 59 + name: 'Quick Stats', 60 + risk_tier: 'low', 61 + canon_ids: [2 as const], 62 + } as const;
+131
examples/todo-app/src/generated/todos/server.ts
··· 1 + /** 2 + * Todos — HTTP Server 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Provides health check, metrics, and module endpoints. 6 + */ 7 + 8 + import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 + 10 + import * as dataIntegrity from './data-integrity.js'; 11 + import * as filteringAndViews from './filtering-and-views.js'; 12 + import * as integration from './integration.js'; 13 + import * as projects from './projects.js'; 14 + import * as quickStats from './quick-stats.js'; 15 + import * as tasks from './tasks.js'; 16 + import * as webExperience from './web-experience.js'; 17 + 18 + // ─── Metrics ───────────────────────────────────────────────────────────────── 19 + 20 + const _svcMetrics = { 21 + requests_total: 0, 22 + requests_by_path: {} as Record<string, number>, 23 + errors_total: 0, 24 + uptime_start: Date.now(), 25 + }; 26 + 27 + // ─── Module Registry ───────────────────────────────────────────────────────── 28 + 29 + const _svcModules = { 30 + 'data-integrity': dataIntegrity, 31 + 'filtering-and-views': filteringAndViews, 32 + 'integration': integration, 33 + 'projects': projects, 34 + 'quick-stats': quickStats, 35 + 'tasks': tasks, 36 + 'web-experience': webExperience, 37 + }; 38 + 39 + // ─── Router ────────────────────────────────────────────────────────────────── 40 + 41 + type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 42 + 43 + const routes: Record<string, Handler> = { 44 + '/health': (_req, res) => { 45 + res.writeHead(200, { 'Content-Type': 'application/json' }); 46 + res.end(JSON.stringify({ 47 + status: 'ok', 48 + service: 'Todos', 49 + uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 50 + modules: Object.keys(_svcModules), 51 + })); 52 + }, 53 + 54 + '/metrics': (_req, res) => { 55 + res.writeHead(200, { 'Content-Type': 'application/json' }); 56 + res.end(JSON.stringify({ 57 + ..._svcMetrics, 58 + uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 59 + }, null, 2)); 60 + }, 61 + 62 + '/modules': (_req, res) => { 63 + const info = Object.entries(_svcModules).map(([name, mod]) => { 64 + const phoenix = (mod as Record<string, unknown>)._phoenix as Record<string, unknown> | undefined; 65 + return { 66 + name, 67 + risk_tier: phoenix?.risk_tier ?? 'unknown', 68 + exports: Object.keys(mod).filter(k => k !== '_phoenix'), 69 + }; 70 + }); 71 + res.writeHead(200, { 'Content-Type': 'application/json' }); 72 + res.end(JSON.stringify(info, null, 2)); 73 + }, 74 + }; 75 + 76 + // ─── Server ────────────────────────────────────────────────────────────────── 77 + 78 + function handleRequest(req: IncomingMessage, res: ServerResponse): void { 79 + const url = req.url ?? '/'; 80 + const path = url.split('?')[0]; 81 + 82 + _svcMetrics.requests_total++; 83 + _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 84 + 85 + const handler = routes[path]; 86 + if (handler) { 87 + try { 88 + handler(req, res); 89 + } catch (err) { 90 + _svcMetrics.errors_total++; 91 + res.writeHead(500, { 'Content-Type': 'application/json' }); 92 + res.end(JSON.stringify({ error: String(err) })); 93 + } 94 + } else { 95 + res.writeHead(404, { 'Content-Type': 'application/json' }); 96 + res.end(JSON.stringify({ 97 + error: 'Not Found', 98 + path, 99 + available: Object.keys(routes), 100 + })); 101 + } 102 + } 103 + 104 + export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 105 + const requestedPort = port ?? parseInt(process.env.TODOS_PORT ?? process.env.PORT ?? '3000', 10); 106 + const server = createServer(handleRequest); 107 + let actualPort = requestedPort; 108 + 109 + const ready = new Promise<void>(resolve => { 110 + server.listen(requestedPort, () => { 111 + const addr = server.address(); 112 + if (addr && typeof addr === 'object') actualPort = addr.port; 113 + result.port = actualPort; 114 + console.log(`Todos listening on http://localhost:${actualPort}`); 115 + console.log(` /health — health check`); 116 + console.log(` /metrics — request metrics`); 117 + console.log(` /modules — registered modules`); 118 + resolve(); 119 + }); 120 + }); 121 + 122 + const result = { server, port: actualPort, ready }; 123 + return result; 124 + } 125 + 126 + // Start when run directly 127 + const isMain = process.argv[1]?.endsWith('/todos/server.js') || 128 + process.argv[1]?.endsWith('/todos/server.ts'); 129 + if (isMain) { 130 + startServer(); 131 + }
+169
examples/todo-app/src/generated/todos/tasks.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register table migration 6 + registerMigration('tasks', ` 7 + CREATE TABLE IF NOT EXISTS tasks ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + title TEXT NOT NULL, 10 + description TEXT DEFAULT '', 11 + priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 12 + due_date TEXT, 13 + completed INTEGER NOT NULL DEFAULT 0, 14 + project_id INTEGER, 15 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 16 + ) 17 + `); 18 + 19 + const CreateTaskSchema = z.object({ 20 + title: z.string().min(1).max(200), 21 + description: z.string().optional().default(''), 22 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional().default('normal'), 23 + due_date: z.string().nullable().optional(), 24 + project_id: z.number().int().nullable().optional(), 25 + }); 26 + 27 + const UpdateTaskSchema = z.object({ 28 + title: z.string().min(1).max(200).optional(), 29 + description: z.string().optional(), 30 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 31 + due_date: z.string().nullable().optional(), 32 + completed: z.number().int().min(0).max(1).optional(), 33 + project_id: z.number().int().nullable().optional(), 34 + }); 35 + 36 + const router = new Hono(); 37 + 38 + // List all tasks with filtering and sorting 39 + router.get('/', (c) => { 40 + let sql = 'SELECT * FROM tasks'; 41 + const conditions: string[] = []; 42 + const params: (string | number)[] = []; 43 + 44 + const status = c.req.query('status'); 45 + if (status === 'active') { 46 + conditions.push('completed = 0'); 47 + } else if (status === 'completed') { 48 + conditions.push('completed = 1'); 49 + } 50 + 51 + const priority = c.req.query('priority'); 52 + if (priority) { 53 + conditions.push('priority = ?'); 54 + params.push(priority); 55 + } 56 + 57 + const projectId = c.req.query('project_id'); 58 + if (projectId) { 59 + conditions.push('project_id = ?'); 60 + params.push(Number(projectId)); 61 + } 62 + 63 + if (conditions.length > 0) { 64 + sql += ' WHERE ' + conditions.join(' AND '); 65 + } 66 + 67 + // Sort by urgency and overdue status first, then by created_at 68 + sql += ` ORDER BY 69 + CASE WHEN due_date < date('now') AND completed = 0 THEN 0 ELSE 1 END, 70 + CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 END, 71 + created_at DESC`; 72 + 73 + const tasks = db.prepare(sql).all(...params); 74 + return c.json(tasks); 75 + }); 76 + 77 + // Get single task 78 + router.get('/:id', (c) => { 79 + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(c.req.param('id')); 80 + if (!task) return c.json({ error: 'Task not found' }, 404); 81 + return c.json(task); 82 + }); 83 + 84 + // Create task 85 + router.post('/', async (c) => { 86 + let body; 87 + try { 88 + body = await c.req.json(); 89 + } catch { 90 + return c.json({ error: 'Invalid JSON' }, 400); 91 + } 92 + 93 + const result = CreateTaskSchema.safeParse(body); 94 + if (!result.success) { 95 + return c.json({ error: result.error.issues[0].message }, 400); 96 + } 97 + 98 + const { title, description, priority, due_date, project_id } = result.data; 99 + 100 + const info = db.prepare(` 101 + INSERT INTO tasks (title, description, priority, due_date, project_id) 102 + VALUES (?, ?, ?, ?, ?) 103 + `).run(title, description, priority, due_date, project_id); 104 + 105 + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(info.lastInsertRowid); 106 + return c.json(task, 201); 107 + }); 108 + 109 + // Update task 110 + router.patch('/:id', async (c) => { 111 + const id = c.req.param('id'); 112 + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 113 + if (!existing) return c.json({ error: 'Task not found' }, 404); 114 + 115 + let body; 116 + try { 117 + body = await c.req.json(); 118 + } catch { 119 + return c.json({ error: 'Invalid JSON' }, 400); 120 + } 121 + 122 + const result = UpdateTaskSchema.safeParse(body); 123 + if (!result.success) { 124 + return c.json({ error: result.error.issues[0].message }, 400); 125 + } 126 + 127 + const updates = result.data; 128 + 129 + if (updates.title !== undefined) { 130 + db.prepare('UPDATE tasks SET title = ? WHERE id = ?').run(updates.title, id); 131 + } 132 + if (updates.description !== undefined) { 133 + db.prepare('UPDATE tasks SET description = ? WHERE id = ?').run(updates.description, id); 134 + } 135 + if (updates.priority !== undefined) { 136 + db.prepare('UPDATE tasks SET priority = ? WHERE id = ?').run(updates.priority, id); 137 + } 138 + if (updates.due_date !== undefined) { 139 + db.prepare('UPDATE tasks SET due_date = ? WHERE id = ?').run(updates.due_date, id); 140 + } 141 + if (updates.completed !== undefined) { 142 + db.prepare('UPDATE tasks SET completed = ? WHERE id = ?').run(updates.completed, id); 143 + } 144 + if (updates.project_id !== undefined) { 145 + db.prepare('UPDATE tasks SET project_id = ? WHERE id = ?').run(updates.project_id, id); 146 + } 147 + 148 + const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 149 + return c.json(updated); 150 + }); 151 + 152 + // Delete task 153 + router.delete('/:id', (c) => { 154 + const id = c.req.param('id'); 155 + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 156 + if (!existing) return c.json({ error: 'Task not found' }, 404); 157 + 158 + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 159 + return c.body(null, 204); 160 + }); 161 + 162 + export default router; 163 + 164 + export const _phoenix = { 165 + iu_id: '1628f3b0f6e0816a698cf8b53a7b135c5dc11469a9bca1fa49299db6018b08f7', 166 + name: 'Tasks', 167 + risk_tier: 'high', 168 + canon_ids: [4 as const], 169 + } as const;
-181
examples/todo-app/src/generated/todos/todos.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - // Register table migrations 6 - registerMigration('categories', ` 7 - CREATE TABLE IF NOT EXISTS categories ( 8 - id INTEGER PRIMARY KEY AUTOINCREMENT, 9 - name TEXT NOT NULL UNIQUE, 10 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 11 - ) 12 - `); 13 - 14 - registerMigration('todos', ` 15 - CREATE TABLE IF NOT EXISTS todos ( 16 - id INTEGER PRIMARY KEY AUTOINCREMENT, 17 - title TEXT NOT NULL, 18 - completed INTEGER NOT NULL DEFAULT 0, 19 - category_id INTEGER REFERENCES categories(id), 20 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 21 - ) 22 - `); 23 - 24 - const CreateTodoSchema = z.object({ 25 - title: z.string().min(1).max(200), 26 - category_id: z.number().int().optional(), 27 - }); 28 - 29 - const UpdateTodoSchema = z.object({ 30 - title: z.string().min(1).max(200).optional(), 31 - completed: z.number().int().min(0).max(1).optional(), 32 - category_id: z.number().int().nullable().optional(), 33 - }); 34 - 35 - const router = new Hono(); 36 - 37 - // List todos with filtering and category names 38 - router.get('/', (c) => { 39 - let sql = 'SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id'; 40 - const conditions: string[] = []; 41 - const params: (string | number)[] = []; 42 - 43 - const completed = c.req.query('completed'); 44 - if (completed !== undefined) { 45 - conditions.push('todos.completed = ?'); 46 - params.push(Number(completed)); 47 - } 48 - 49 - const categoryId = c.req.query('category_id'); 50 - if (categoryId !== undefined) { 51 - conditions.push('todos.category_id = ?'); 52 - params.push(Number(categoryId)); 53 - } 54 - 55 - if (conditions.length > 0) { 56 - sql += ' WHERE ' + conditions.join(' AND '); 57 - } 58 - sql += ' ORDER BY todos.created_at DESC'; 59 - 60 - return c.json(db.prepare(sql).all(...params)); 61 - }); 62 - 63 - // Get stats 64 - router.get('/stats', (c) => { 65 - const total = db.prepare('SELECT COUNT(*) as count FROM todos').get() as { count: number }; 66 - const completed = db.prepare('SELECT COUNT(*) as count FROM todos WHERE completed = 1').get() as { count: number }; 67 - const incomplete = db.prepare('SELECT COUNT(*) as count FROM todos WHERE completed = 0').get() as { count: number }; 68 - 69 - const byCategory = db.prepare(` 70 - SELECT categories.name as category_name, COUNT(todos.id) as count 71 - FROM categories 72 - LEFT JOIN todos ON categories.id = todos.category_id 73 - GROUP BY categories.id, categories.name 74 - ORDER BY count DESC 75 - `).all() as { category_name: string; count: number }[]; 76 - 77 - return c.json({ 78 - total: total.count, 79 - completed: completed.count, 80 - incomplete: incomplete.count, 81 - by_category: byCategory 82 - }); 83 - }); 84 - 85 - // Get single todo 86 - router.get('/:id', (c) => { 87 - const todo = db.prepare('SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id WHERE todos.id = ?').get(c.req.param('id')); 88 - if (!todo) return c.json({ error: 'Not found' }, 404); 89 - return c.json(todo); 90 - }); 91 - 92 - // Create todo 93 - router.post('/', async (c) => { 94 - let body; 95 - try { 96 - body = await c.req.json(); 97 - } catch { 98 - return c.json({ error: 'Invalid JSON' }, 400); 99 - } 100 - 101 - const result = CreateTodoSchema.safeParse(body); 102 - if (!result.success) { 103 - return c.json({ error: result.error.issues[0].message }, 400); 104 - } 105 - 106 - const { title, category_id } = result.data; 107 - 108 - // Validate category exists if provided 109 - if (category_id !== undefined) { 110 - const category = db.prepare('SELECT id FROM categories WHERE id = ?').get(category_id); 111 - if (!category) { 112 - return c.json({ error: 'Category not found' }, 400); 113 - } 114 - } 115 - 116 - const info = db.prepare('INSERT INTO todos (title, category_id) VALUES (?, ?)').run(title, category_id ?? null); 117 - const todo = db.prepare('SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id WHERE todos.id = ?').get(info.lastInsertRowid); 118 - return c.json(todo, 201); 119 - }); 120 - 121 - // Update todo 122 - router.patch('/:id', async (c) => { 123 - const id = c.req.param('id'); 124 - const existing = db.prepare('SELECT id FROM todos WHERE id = ?').get(id); 125 - if (!existing) return c.json({ error: 'Not found' }, 404); 126 - 127 - let body; 128 - try { 129 - body = await c.req.json(); 130 - } catch { 131 - return c.json({ error: 'Invalid JSON' }, 400); 132 - } 133 - 134 - const result = UpdateTodoSchema.safeParse(body); 135 - if (!result.success) { 136 - return c.json({ error: result.error.issues[0].message }, 400); 137 - } 138 - 139 - const updates = result.data; 140 - 141 - // Validate category exists if provided 142 - if (updates.category_id !== undefined && updates.category_id !== null) { 143 - const category = db.prepare('SELECT id FROM categories WHERE id = ?').get(updates.category_id); 144 - if (!category) { 145 - return c.json({ error: 'Category not found' }, 400); 146 - } 147 - } 148 - 149 - if (updates.title !== undefined) { 150 - db.prepare('UPDATE todos SET title = ? WHERE id = ?').run(updates.title, id); 151 - } 152 - if (updates.completed !== undefined) { 153 - db.prepare('UPDATE todos SET completed = ? WHERE id = ?').run(updates.completed, id); 154 - } 155 - if (updates.category_id !== undefined) { 156 - db.prepare('UPDATE todos SET category_id = ? WHERE id = ?').run(updates.category_id, id); 157 - } 158 - 159 - const updated = db.prepare('SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id WHERE todos.id = ?').get(id); 160 - return c.json(updated); 161 - }); 162 - 163 - // Delete todo 164 - router.delete('/:id', (c) => { 165 - const id = c.req.param('id'); 166 - const existing = db.prepare('SELECT id FROM todos WHERE id = ?').get(id); 167 - if (!existing) return c.json({ error: 'Not found' }, 404); 168 - 169 - db.prepare('DELETE FROM todos WHERE id = ?').run(id); 170 - return c.body(null, 204); 171 - }); 172 - 173 - export default router; 174 - 175 - /** @internal Phoenix VCS traceability — do not remove. */ 176 - export const _phoenix = { 177 - iu_id: '614d1c26e17fec59d237b38cb78d14045816af536a424d086aee82f154b0e287', 178 - name: 'Todos', 179 - risk_tier: 'high', 180 - canon_ids: [13 as const], 181 - } as const;
+909
examples/todo-app/src/generated/todos/web-experience.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register migrations for all tables this module touches 6 + registerMigration('projects', ` 7 + CREATE TABLE IF NOT EXISTS projects ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('tasks', ` 16 + CREATE TABLE IF NOT EXISTS tasks ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL, 19 + description TEXT NOT NULL DEFAULT '', 20 + priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 + due_date TEXT, 22 + completed INTEGER NOT NULL DEFAULT 0, 23 + project_id INTEGER REFERENCES projects(id), 24 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 25 + ) 26 + `); 27 + 28 + const createTaskSchema = z.object({ 29 + title: z.string().min(1), 30 + description: z.string().default(''), 31 + priority: z.enum(['urgent', 'high', 'normal', 'low']).default('normal'), 32 + due_date: z.string().nullable().optional(), 33 + project_id: z.number().nullable().optional() 34 + }); 35 + 36 + const updateTaskSchema = z.object({ 37 + title: z.string().min(1).optional(), 38 + description: z.string().optional(), 39 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 40 + due_date: z.string().nullable().optional(), 41 + completed: z.number().min(0).max(1).optional(), 42 + project_id: z.number().nullable().optional() 43 + }); 44 + 45 + const createProjectSchema = z.object({ 46 + name: z.string().min(1), 47 + color: z.string().default('#3b82f6') 48 + }); 49 + 50 + const router = new Hono(); 51 + 52 + // Web interface route 53 + router.get('/', (c) => { 54 + return c.html(`<!DOCTYPE html> 55 + <html lang="en"> 56 + <head> 57 + <meta charset="UTF-8"> 58 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 59 + <title>Task Manager</title> 60 + <style> 61 + * { 62 + margin: 0; 63 + padding: 0; 64 + box-sizing: border-box; 65 + } 66 + 67 + body { 68 + font-family: system-ui, -apple-system, sans-serif; 69 + background-color: #f8fafc; 70 + color: #1e293b; 71 + line-height: 1.6; 72 + } 73 + 74 + .container { 75 + max-width: 800px; 76 + margin: 0 auto; 77 + padding: 20px; 78 + } 79 + 80 + .header { 81 + text-align: center; 82 + margin-bottom: 30px; 83 + } 84 + 85 + .header h1 { 86 + color: #0f172a; 87 + margin-bottom: 10px; 88 + } 89 + 90 + .stats { 91 + display: flex; 92 + gap: 20px; 93 + justify-content: center; 94 + margin-bottom: 30px; 95 + flex-wrap: wrap; 96 + } 97 + 98 + .stat-card { 99 + background: white; 100 + padding: 15px 20px; 101 + border-radius: 8px; 102 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 103 + text-align: center; 104 + min-width: 120px; 105 + } 106 + 107 + .stat-number { 108 + font-size: 24px; 109 + font-weight: bold; 110 + color: #3b82f6; 111 + } 112 + 113 + .stat-label { 114 + font-size: 14px; 115 + color: #64748b; 116 + } 117 + 118 + .add-task-form { 119 + background: white; 120 + padding: 20px; 121 + border-radius: 8px; 122 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 123 + margin-bottom: 30px; 124 + } 125 + 126 + .form-row { 127 + display: flex; 128 + gap: 15px; 129 + margin-bottom: 15px; 130 + flex-wrap: wrap; 131 + } 132 + 133 + .form-group { 134 + flex: 1; 135 + min-width: 200px; 136 + } 137 + 138 + .form-group.full-width { 139 + flex: 100%; 140 + } 141 + 142 + label { 143 + display: block; 144 + margin-bottom: 5px; 145 + font-weight: 500; 146 + color: #374151; 147 + } 148 + 149 + input, textarea, select { 150 + width: 100%; 151 + padding: 8px 12px; 152 + border: 1px solid #d1d5db; 153 + border-radius: 6px; 154 + font-size: 14px; 155 + } 156 + 157 + textarea { 158 + resize: vertical; 159 + min-height: 80px; 160 + } 161 + 162 + .description-toggle { 163 + background: none; 164 + border: none; 165 + color: #3b82f6; 166 + cursor: pointer; 167 + font-size: 14px; 168 + text-decoration: underline; 169 + margin-bottom: 10px; 170 + } 171 + 172 + .description-section { 173 + display: none; 174 + } 175 + 176 + .description-section.expanded { 177 + display: block; 178 + } 179 + 180 + .btn { 181 + background: #3b82f6; 182 + color: white; 183 + border: none; 184 + padding: 10px 20px; 185 + border-radius: 6px; 186 + cursor: pointer; 187 + font-size: 14px; 188 + font-weight: 500; 189 + } 190 + 191 + .btn:hover { 192 + background: #2563eb; 193 + } 194 + 195 + .filters { 196 + display: flex; 197 + gap: 15px; 198 + margin-bottom: 20px; 199 + flex-wrap: wrap; 200 + align-items: center; 201 + } 202 + 203 + .filter-group { 204 + display: flex; 205 + gap: 5px; 206 + } 207 + 208 + .filter-btn { 209 + padding: 6px 12px; 210 + border: 1px solid #d1d5db; 211 + background: white; 212 + border-radius: 6px; 213 + cursor: pointer; 214 + font-size: 14px; 215 + } 216 + 217 + .filter-btn.active { 218 + background: #3b82f6; 219 + color: white; 220 + border-color: #3b82f6; 221 + } 222 + 223 + .task-list { 224 + background: white; 225 + border-radius: 8px; 226 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 227 + } 228 + 229 + .task-item { 230 + padding: 15px 20px; 231 + border-bottom: 1px solid #e5e7eb; 232 + position: relative; 233 + transition: background-color 0.2s; 234 + } 235 + 236 + .task-item:last-child { 237 + border-bottom: none; 238 + } 239 + 240 + .task-item:hover { 241 + background-color: #f9fafb; 242 + } 243 + 244 + .task-item.completed { 245 + opacity: 0.6; 246 + } 247 + 248 + .task-item.completed .task-title { 249 + text-decoration: line-through; 250 + } 251 + 252 + .task-item.overdue { 253 + border-left: 4px solid #ef4444; 254 + } 255 + 256 + .task-header { 257 + display: flex; 258 + align-items: center; 259 + gap: 10px; 260 + margin-bottom: 8px; 261 + } 262 + 263 + .task-checkbox { 264 + width: 18px; 265 + height: 18px; 266 + cursor: pointer; 267 + } 268 + 269 + .task-title { 270 + font-weight: 500; 271 + flex: 1; 272 + } 273 + 274 + .priority-badge { 275 + padding: 2px 8px; 276 + border-radius: 12px; 277 + font-size: 12px; 278 + font-weight: 500; 279 + } 280 + 281 + .priority-urgent { 282 + background: #fee2e2; 283 + color: #dc2626; 284 + } 285 + 286 + .priority-high { 287 + background: #fed7aa; 288 + color: #ea580c; 289 + } 290 + 291 + .priority-normal { 292 + background: #dbeafe; 293 + color: #2563eb; 294 + } 295 + 296 + .priority-low { 297 + background: #f3f4f6; 298 + color: #6b7280; 299 + } 300 + 301 + .task-meta { 302 + display: flex; 303 + gap: 15px; 304 + font-size: 14px; 305 + color: #64748b; 306 + flex-wrap: wrap; 307 + } 308 + 309 + .project-name { 310 + color: #3b82f6; 311 + } 312 + 313 + .due-date { 314 + color: #059669; 315 + } 316 + 317 + .overdue-badge { 318 + color: #dc2626; 319 + font-weight: 500; 320 + } 321 + 322 + .task-description { 323 + margin-top: 8px; 324 + color: #64748b; 325 + font-size: 14px; 326 + } 327 + 328 + .delete-btn { 329 + position: absolute; 330 + right: 15px; 331 + top: 50%; 332 + transform: translateY(-50%); 333 + background: #ef4444; 334 + color: white; 335 + border: none; 336 + padding: 6px 10px; 337 + border-radius: 4px; 338 + cursor: pointer; 339 + font-size: 12px; 340 + opacity: 0; 341 + transition: opacity 0.2s; 342 + } 343 + 344 + .task-item:hover .delete-btn { 345 + opacity: 1; 346 + } 347 + 348 + .delete-btn:hover { 349 + background: #dc2626; 350 + } 351 + 352 + .empty-state { 353 + text-align: center; 354 + padding: 40px 20px; 355 + color: #64748b; 356 + } 357 + 358 + @media (max-width: 600px) { 359 + .container { 360 + padding: 15px; 361 + } 362 + 363 + .form-row { 364 + flex-direction: column; 365 + } 366 + 367 + .form-group { 368 + min-width: auto; 369 + } 370 + 371 + .filters { 372 + flex-direction: column; 373 + align-items: stretch; 374 + } 375 + 376 + .stats { 377 + flex-direction: column; 378 + } 379 + } 380 + </style> 381 + </head> 382 + <body> 383 + <div class="container"> 384 + <div class="header"> 385 + <h1>Task Manager</h1> 386 + <div class="stats" id="stats"> 387 + <!-- Stats will be populated by JavaScript --> 388 + </div> 389 + </div> 390 + 391 + <div class="add-task-form"> 392 + <form id="addTaskForm"> 393 + <div class="form-row"> 394 + <div class="form-group"> 395 + <label for="title">Task Title</label> 396 + <input type="text" id="title" name="title" required> 397 + </div> 398 + <div class="form-group"> 399 + <label for="priority">Priority</label> 400 + <select id="priority" name="priority"> 401 + <option value="normal">Normal</option> 402 + <option value="low">Low</option> 403 + <option value="high">High</option> 404 + <option value="urgent">Urgent</option> 405 + </select> 406 + </div> 407 + </div> 408 + 409 + <div class="form-row"> 410 + <div class="form-group"> 411 + <label for="project">Project</label> 412 + <select id="project" name="project_id"> 413 + <option value="">Inbox</option> 414 + </select> 415 + </div> 416 + <div class="form-group"> 417 + <label for="dueDate">Due Date</label> 418 + <input type="date" id="dueDate" name="due_date"> 419 + </div> 420 + </div> 421 + 422 + <button type="button" class="description-toggle" onclick="toggleDescription()"> 423 + + Add Description 424 + </button> 425 + 426 + <div class="description-section" id="descriptionSection"> 427 + <div class="form-group full-width"> 428 + <label for="description">Description</label> 429 + <textarea id="description" name="description" placeholder="Optional task description..."></textarea> 430 + </div> 431 + </div> 432 + 433 + <button type="submit" class="btn">Add Task</button> 434 + </form> 435 + </div> 436 + 437 + <div class="filters"> 438 + <div class="filter-group"> 439 + <button class="filter-btn active" data-status="all">All</button> 440 + <button class="filter-btn" data-status="active">Active</button> 441 + <button class="filter-btn" data-status="completed">Completed</button> 442 + </div> 443 + 444 + <div class="form-group" style="min-width: 150px;"> 445 + <select id="priorityFilter"> 446 + <option value="">All Priorities</option> 447 + <option value="urgent">Urgent</option> 448 + <option value="high">High</option> 449 + <option value="normal">Normal</option> 450 + <option value="low">Low</option> 451 + </select> 452 + </div> 453 + 454 + <div class="form-group" style="min-width: 150px;"> 455 + <select id="projectFilter"> 456 + <option value="">All Projects</option> 457 + </select> 458 + </div> 459 + </div> 460 + 461 + <div class="task-list" id="taskList"> 462 + <!-- Tasks will be populated by JavaScript --> 463 + </div> 464 + </div> 465 + 466 + <script> 467 + let currentFilters = { 468 + status: 'all', 469 + priority: '', 470 + project: '' 471 + }; 472 + 473 + // Initialize the app 474 + document.addEventListener('DOMContentLoaded', function() { 475 + loadProjects(); 476 + loadTasks(); 477 + loadStats(); 478 + setupEventListeners(); 479 + }); 480 + 481 + function setupEventListeners() { 482 + // Add task form 483 + document.getElementById('addTaskForm').addEventListener('submit', handleAddTask); 484 + 485 + // Status filters 486 + document.querySelectorAll('[data-status]').forEach(btn => { 487 + btn.addEventListener('click', function() { 488 + document.querySelectorAll('[data-status]').forEach(b => b.classList.remove('active')); 489 + this.classList.add('active'); 490 + currentFilters.status = this.dataset.status; 491 + loadTasks(); 492 + }); 493 + }); 494 + 495 + // Priority filter 496 + document.getElementById('priorityFilter').addEventListener('change', function() { 497 + currentFilters.priority = this.value; 498 + loadTasks(); 499 + }); 500 + 501 + // Project filter 502 + document.getElementById('projectFilter').addEventListener('change', function() { 503 + currentFilters.project = this.value; 504 + loadTasks(); 505 + }); 506 + } 507 + 508 + function toggleDescription() { 509 + const section = document.getElementById('descriptionSection'); 510 + const toggle = document.querySelector('.description-toggle'); 511 + 512 + if (section.classList.contains('expanded')) { 513 + section.classList.remove('expanded'); 514 + toggle.textContent = '+ Add Description'; 515 + } else { 516 + section.classList.add('expanded'); 517 + toggle.textContent = '- Hide Description'; 518 + } 519 + } 520 + 521 + async function loadProjects() { 522 + try { 523 + const response = await fetch('/api/projects'); 524 + const projects = await response.json(); 525 + 526 + const projectSelect = document.getElementById('project'); 527 + const projectFilter = document.getElementById('projectFilter'); 528 + 529 + // Clear existing options (except first) 530 + projectSelect.innerHTML = '<option value="">Inbox</option>'; 531 + projectFilter.innerHTML = '<option value="">All Projects</option>'; 532 + 533 + projects.forEach(project => { 534 + const option1 = document.createElement('option'); 535 + option1.value = project.id; 536 + option1.textContent = project.name; 537 + projectSelect.appendChild(option1); 538 + 539 + const option2 = document.createElement('option'); 540 + option2.value = project.id; 541 + option2.textContent = project.name; 542 + projectFilter.appendChild(option2); 543 + }); 544 + } catch (error) { 545 + console.error('Error loading projects:', error); 546 + } 547 + } 548 + 549 + async function loadTasks() { 550 + try { 551 + const params = new URLSearchParams(); 552 + 553 + if (currentFilters.status === 'active') { 554 + params.append('completed', '0'); 555 + } else if (currentFilters.status === 'completed') { 556 + params.append('completed', '1'); 557 + } 558 + 559 + if (currentFilters.priority) { 560 + params.append('priority', currentFilters.priority); 561 + } 562 + 563 + if (currentFilters.project) { 564 + params.append('project_id', currentFilters.project); 565 + } 566 + 567 + const response = await fetch('/api/tasks?' + params.toString()); 568 + const tasks = await response.json(); 569 + 570 + renderTasks(tasks); 571 + loadStats(); // Refresh stats after loading tasks 572 + } catch (error) { 573 + console.error('Error loading tasks:', error); 574 + } 575 + } 576 + 577 + function renderTasks(tasks) { 578 + const taskList = document.getElementById('taskList'); 579 + 580 + if (tasks.length === 0) { 581 + taskList.innerHTML = '<div class="empty-state">No tasks found. Add your first task above!</div>'; 582 + return; 583 + } 584 + 585 + const now = new Date().toISOString().split('T')[0]; 586 + 587 + taskList.innerHTML = tasks.map(task => { 588 + const isOverdue = task.due_date && task.due_date < now && !task.completed; 589 + const priorityClass = 'priority-' + task.priority; 590 + 591 + return \` 592 + <div class="task-item \${task.completed ? 'completed' : ''} \${isOverdue ? 'overdue' : ''}" data-id="\${task.id}"> 593 + <div class="task-header"> 594 + <input type="checkbox" class="task-checkbox" \${task.completed ? 'checked' : ''} 595 + onchange="toggleTask(\${task.id}, this.checked)"> 596 + <div class="task-title">\${escapeHtml(task.title)}</div> 597 + <span class="priority-badge \${priorityClass}">\${task.priority}</span> 598 + </div> 599 + 600 + <div class="task-meta"> 601 + \${task.project_name ? \`<span class="project-name">\${escapeHtml(task.project_name)}</span>\` : '<span class="project-name">Inbox</span>'} 602 + \${task.due_date ? (isOverdue ? \`<span class="overdue-badge">Overdue (\${formatDate(task.due_date)})</span>\` : \`<span class="due-date">Due \${formatDate(task.due_date)}</span>\`) : ''} 603 + </div> 604 + 605 + \${task.description ? \`<div class="task-description">\${escapeHtml(task.description)}</div>\` : ''} 606 + 607 + <button class="delete-btn" onclick="deleteTask(\${task.id})">Delete</button> 608 + </div> 609 + \`; 610 + }).join(''); 611 + } 612 + 613 + async function loadStats() { 614 + try { 615 + const response = await fetch('/api/tasks/stats'); 616 + const stats = await response.json(); 617 + 618 + const statsContainer = document.getElementById('stats'); 619 + statsContainer.innerHTML = \` 620 + <div class="stat-card"> 621 + <div class="stat-number">\${stats.total_tasks}</div> 622 + <div class="stat-label">Total</div> 623 + </div> 624 + <div class="stat-card"> 625 + <div class="stat-number">\${stats.active_tasks}</div> 626 + <div class="stat-label">Active</div> 627 + </div> 628 + <div class="stat-card"> 629 + <div class="stat-number">\${stats.completed_tasks}</div> 630 + <div class="stat-label">Completed</div> 631 + </div> 632 + <div class="stat-card"> 633 + <div class="stat-number">\${stats.overdue_tasks}</div> 634 + <div class="stat-label">Overdue</div> 635 + </div> 636 + \`; 637 + } catch (error) { 638 + console.error('Error loading stats:', error); 639 + } 640 + } 641 + 642 + async function handleAddTask(e) { 643 + e.preventDefault(); 644 + 645 + const formData = new FormData(e.target); 646 + const taskData = { 647 + title: formData.get('title'), 648 + description: formData.get('description') || '', 649 + priority: formData.get('priority'), 650 + due_date: formData.get('due_date') || null, 651 + project_id: formData.get('project_id') ? parseInt(formData.get('project_id')) : null 652 + }; 653 + 654 + try { 655 + const response = await fetch('/api/tasks', { 656 + method: 'POST', 657 + headers: { 'Content-Type': 'application/json' }, 658 + body: JSON.stringify(taskData) 659 + }); 660 + 661 + if (response.ok) { 662 + e.target.reset(); 663 + document.getElementById('descriptionSection').classList.remove('expanded'); 664 + document.querySelector('.description-toggle').textContent = '+ Add Description'; 665 + loadTasks(); 666 + } else { 667 + const error = await response.json(); 668 + alert('Error: ' + error.error); 669 + } 670 + } catch (error) { 671 + console.error('Error adding task:', error); 672 + alert('Error adding task'); 673 + } 674 + } 675 + 676 + async function toggleTask(id, completed) { 677 + try { 678 + const response = await fetch(\`/api/tasks/\${id}\`, { 679 + method: 'PATCH', 680 + headers: { 'Content-Type': 'application/json' }, 681 + body: JSON.stringify({ completed: completed ? 1 : 0 }) 682 + }); 683 + 684 + if (response.ok) { 685 + loadTasks(); 686 + } else { 687 + console.error('Error toggling task'); 688 + } 689 + } catch (error) { 690 + console.error('Error toggling task:', error); 691 + } 692 + } 693 + 694 + async function deleteTask(id) { 695 + if (!confirm('Are you sure you want to delete this task?')) return; 696 + 697 + try { 698 + const response = await fetch(\`/api/tasks/\${id}\`, { 699 + method: 'DELETE' 700 + }); 701 + 702 + if (response.ok) { 703 + loadTasks(); 704 + } else { 705 + console.error('Error deleting task'); 706 + } 707 + } catch (error) { 708 + console.error('Error deleting task:', error); 709 + } 710 + } 711 + 712 + function escapeHtml(text) { 713 + const div = document.createElement('div'); 714 + div.textContent = text; 715 + return div.innerHTML; 716 + } 717 + 718 + function formatDate(dateString) { 719 + const date = new Date(dateString); 720 + return date.toLocaleDateString(); 721 + } 722 + </script> 723 + </body> 724 + </html>`); 725 + }); 726 + 727 + // API Routes 728 + 729 + // Projects 730 + router.get('/api/projects', (c) => { 731 + const projects = db.prepare('SELECT * FROM projects ORDER BY name').all(); 732 + return c.json(projects); 733 + }); 734 + 735 + router.post('/api/projects', async (c) => { 736 + const body = await c.req.json(); 737 + const result = createProjectSchema.safeParse(body); 738 + 739 + if (!result.success) { 740 + return c.json({ error: 'Invalid project data' }, 400); 741 + } 742 + 743 + try { 744 + const stmt = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)'); 745 + const info = stmt.run(result.data.name, result.data.color); 746 + 747 + const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(info.lastInsertRowid); 748 + return c.json(project, 201); 749 + } catch (error) { 750 + return c.json({ error: 'Project name already exists' }, 400); 751 + } 752 + }); 753 + 754 + // Tasks 755 + router.get('/api/tasks', (c) => { 756 + const { completed, priority, project_id } = c.req.query(); 757 + 758 + let sql = ` 759 + SELECT t.*, p.name as project_name 760 + FROM tasks t 761 + LEFT JOIN projects p ON t.project_id = p.id 762 + WHERE 1=1 763 + `; 764 + const params: unknown[] = []; 765 + 766 + if (completed !== undefined) { 767 + sql += ' AND t.completed = ?'; 768 + params.push(parseInt(completed)); 769 + } 770 + 771 + if (priority) { 772 + sql += ' AND t.priority = ?'; 773 + params.push(priority); 774 + } 775 + 776 + if (project_id) { 777 + sql += ' AND t.project_id = ?'; 778 + params.push(parseInt(project_id)); 779 + } 780 + 781 + sql += ' ORDER BY t.completed ASC, t.priority = "urgent" DESC, t.priority = "high" DESC, t.due_date ASC, t.created_at DESC'; 782 + 783 + const tasks = db.prepare(sql).all(...params); 784 + return c.json(tasks); 785 + }); 786 + 787 + router.get('/api/tasks/stats', (c) => { 788 + const stats = db.prepare(` 789 + SELECT 790 + COUNT(*) as total_tasks, 791 + SUM(CASE WHEN completed = 0 THEN 1 ELSE 0 END) as active_tasks, 792 + SUM(CASE WHEN completed = 1 THEN 1 ELSE 0 END) as completed_tasks, 793 + SUM(CASE WHEN completed = 0 AND due_date IS NOT NULL AND due_date < date('now') THEN 1 ELSE 0 END) as overdue_tasks 794 + FROM tasks 795 + `).get(); 796 + 797 + return c.json(stats); 798 + }); 799 + 800 + router.get('/api/tasks/:id', (c) => { 801 + const id = parseInt(c.req.param('id')); 802 + const task = db.prepare(` 803 + SELECT t.*, p.name as project_name 804 + FROM tasks t 805 + LEFT JOIN projects p ON t.project_id = p.id 806 + WHERE t.id = ? 807 + `).get(id); 808 + 809 + if (!task) { 810 + return c.json({ error: 'Task not found' }, 404); 811 + } 812 + 813 + return c.json(task); 814 + }); 815 + 816 + router.post('/api/tasks', async (c) => { 817 + const body = await c.req.json(); 818 + const result = createTaskSchema.safeParse(body); 819 + 820 + if (!result.success) { 821 + return c.json({ error: 'Invalid task data' }, 400); 822 + } 823 + 824 + const stmt = db.prepare(` 825 + INSERT INTO tasks (title, description, priority, due_date, project_id) 826 + VALUES (?, ?, ?, ?, ?) 827 + `); 828 + 829 + const info = stmt.run( 830 + result.data.title, 831 + result.data.description, 832 + result.data.priority, 833 + result.data.due_date || null, 834 + result.data.project_id || null 835 + ); 836 + 837 + const task = db.prepare(` 838 + SELECT t.*, p.name as project_name 839 + FROM tasks t 840 + LEFT JOIN projects p ON t.project_id = p.id 841 + WHERE t.id = ? 842 + `).get(info.lastInsertRowid); 843 + 844 + return c.json(task, 201); 845 + }); 846 + 847 + router.patch('/api/tasks/:id', async (c) => { 848 + const id = parseInt(c.req.param('id')); 849 + const body = await c.req.json(); 850 + const result = updateTaskSchema.safeParse(body); 851 + 852 + if (!result.success) { 853 + return c.json({ error: 'Invalid task data' }, 400); 854 + } 855 + 856 + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 857 + if (!existing) { 858 + return c.json({ error: 'Task not found' }, 404); 859 + } 860 + 861 + const updates: string[] = []; 862 + const params: unknown[] = []; 863 + 864 + Object.entries(result.data).forEach(([key, value]) => { 865 + if (value !== undefined) { 866 + updates.push(`${key} = ?`); 867 + params.push(value); 868 + } 869 + }); 870 + 871 + if (updates.length === 0) { 872 + return c.json({ error: 'No valid fields to update' }, 400); 873 + } 874 + 875 + params.push(id); 876 + const sql = `UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`; 877 + db.prepare(sql).run(...params); 878 + 879 + const task = db.prepare(` 880 + SELECT t.*, p.name as project_name 881 + FROM tasks t 882 + LEFT JOIN projects p ON t.project_id = p.id 883 + WHERE t.id = ? 884 + `).get(id); 885 + 886 + return c.json(task); 887 + }); 888 + 889 + router.delete('/api/tasks/:id', (c) => { 890 + const id = parseInt(c.req.param('id')); 891 + 892 + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 893 + if (!existing) { 894 + return c.json({ error: 'Task not found' }, 404); 895 + } 896 + 897 + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 898 + return c.body(null, 204); 899 + }); 900 + 901 + export default router; 902 + 903 + /** @internal Phoenix VCS traceability — do not remove. */ 904 + export const _phoenix = { 905 + iu_id: '335590ecf9457e5b14124f79e4d9399888f58b7aff87edd6a264b6aa6fdc2d48', 906 + name: 'Web Experience', 907 + risk_tier: 'high', 908 + canon_ids: [5 as const], 909 + } as const;
-710
examples/todo-app/src/generated/todos/web-interface.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - // Register table migrations 6 - registerMigration('categories', ` 7 - CREATE TABLE IF NOT EXISTS categories ( 8 - id INTEGER PRIMARY KEY AUTOINCREMENT, 9 - name TEXT NOT NULL UNIQUE, 10 - color TEXT NOT NULL DEFAULT '#3b82f6', 11 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 - ) 13 - `); 14 - 15 - registerMigration('todos', ` 16 - CREATE TABLE IF NOT EXISTS todos ( 17 - id INTEGER PRIMARY KEY AUTOINCREMENT, 18 - title TEXT NOT NULL, 19 - completed INTEGER NOT NULL DEFAULT 0, 20 - category_id INTEGER REFERENCES categories(id), 21 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 22 - ) 23 - `); 24 - 25 - const router = new Hono(); 26 - 27 - // Validation schemas 28 - const createTodoSchema = z.object({ 29 - title: z.string().min(1), 30 - category_id: z.number().int().positive().optional() 31 - }); 32 - 33 - const updateTodoSchema = z.object({ 34 - title: z.string().min(1).optional(), 35 - completed: z.number().int().min(0).max(1).optional(), 36 - category_id: z.number().int().positive().optional().nullable() 37 - }); 38 - 39 - const createCategorySchema = z.object({ 40 - name: z.string().min(1), 41 - color: z.string().regex(/^#[0-9a-fA-F]{6}$/) 42 - }); 43 - 44 - // Categories routes 45 - router.get('/categories', (c) => { 46 - const stmt = db.prepare('SELECT * FROM categories ORDER BY name'); 47 - const categories = stmt.all(); 48 - return c.json(categories); 49 - }); 50 - 51 - router.get('/categories/:id', (c) => { 52 - const id = parseInt(c.req.param('id')); 53 - if (isNaN(id)) { 54 - return c.json({ error: 'Invalid ID' }, 400); 55 - } 56 - 57 - const stmt = db.prepare('SELECT * FROM categories WHERE id = ?'); 58 - const category = stmt.get(id); 59 - 60 - if (!category) { 61 - return c.json({ error: 'Not found' }, 404); 62 - } 63 - 64 - return c.json(category); 65 - }); 66 - 67 - router.post('/categories', async (c) => { 68 - const body = await c.req.json(); 69 - const result = createCategorySchema.safeParse(body); 70 - 71 - if (!result.success) { 72 - return c.json({ error: 'Invalid input' }, 400); 73 - } 74 - 75 - try { 76 - const stmt = db.prepare('INSERT INTO categories (name, color) VALUES (?, ?)'); 77 - const info = stmt.run(result.data.name, result.data.color); 78 - 79 - const getStmt = db.prepare('SELECT * FROM categories WHERE id = ?'); 80 - const category = getStmt.get(info.lastInsertRowid); 81 - 82 - return c.json(category, 201); 83 - } catch (error) { 84 - return c.json({ error: 'Category name already exists' }, 400); 85 - } 86 - }); 87 - 88 - router.delete('/categories/:id', (c) => { 89 - const id = parseInt(c.req.param('id')); 90 - if (isNaN(id)) { 91 - return c.json({ error: 'Invalid ID' }, 400); 92 - } 93 - 94 - const stmt = db.prepare('DELETE FROM categories WHERE id = ?'); 95 - const info = stmt.run(id); 96 - 97 - if (info.changes === 0) { 98 - return c.json({ error: 'Not found' }, 404); 99 - } 100 - 101 - return c.body(null, 204); 102 - }); 103 - 104 - // Todos routes 105 - router.get('/todos', (c) => { 106 - const stmt = db.prepare(` 107 - SELECT t.*, c.name as category_name, c.color as category_color 108 - FROM todos t 109 - LEFT JOIN categories c ON t.category_id = c.id 110 - ORDER BY t.created_at DESC 111 - `); 112 - const todos = stmt.all(); 113 - return c.json(todos); 114 - }); 115 - 116 - router.get('/todos/:id', (c) => { 117 - const id = parseInt(c.req.param('id')); 118 - if (isNaN(id)) { 119 - return c.json({ error: 'Invalid ID' }, 400); 120 - } 121 - 122 - const stmt = db.prepare(` 123 - SELECT t.*, c.name as category_name, c.color as category_color 124 - FROM todos t 125 - LEFT JOIN categories c ON t.category_id = c.id 126 - WHERE t.id = ? 127 - `); 128 - const todo = stmt.get(id); 129 - 130 - if (!todo) { 131 - return c.json({ error: 'Not found' }, 404); 132 - } 133 - 134 - return c.json(todo); 135 - }); 136 - 137 - router.post('/todos', async (c) => { 138 - const body = await c.req.json(); 139 - const result = createTodoSchema.safeParse(body); 140 - 141 - if (!result.success) { 142 - return c.json({ error: 'Invalid input' }, 400); 143 - } 144 - 145 - const stmt = db.prepare('INSERT INTO todos (title, category_id) VALUES (?, ?)'); 146 - const info = stmt.run(result.data.title, result.data.category_id || null); 147 - 148 - const getStmt = db.prepare(` 149 - SELECT t.*, c.name as category_name, c.color as category_color 150 - FROM todos t 151 - LEFT JOIN categories c ON t.category_id = c.id 152 - WHERE t.id = ? 153 - `); 154 - const todo = getStmt.get(info.lastInsertRowid); 155 - 156 - return c.json(todo, 201); 157 - }); 158 - 159 - router.patch('/todos/:id', async (c) => { 160 - const id = parseInt(c.req.param('id')); 161 - if (isNaN(id)) { 162 - return c.json({ error: 'Invalid ID' }, 400); 163 - } 164 - 165 - const body = await c.req.json(); 166 - const result = updateTodoSchema.safeParse(body); 167 - 168 - if (!result.success) { 169 - return c.json({ error: 'Invalid input' }, 400); 170 - } 171 - 172 - const updates: string[] = []; 173 - const values: any[] = []; 174 - 175 - if (result.data.title !== undefined) { 176 - updates.push('title = ?'); 177 - values.push(result.data.title); 178 - } 179 - 180 - if (result.data.completed !== undefined) { 181 - updates.push('completed = ?'); 182 - values.push(result.data.completed); 183 - } 184 - 185 - if (result.data.category_id !== undefined) { 186 - updates.push('category_id = ?'); 187 - values.push(result.data.category_id); 188 - } 189 - 190 - if (updates.length === 0) { 191 - return c.json({ error: 'No fields to update' }, 400); 192 - } 193 - 194 - values.push(id); 195 - const stmt = db.prepare(`UPDATE todos SET ${updates.join(', ')} WHERE id = ?`); 196 - const info = stmt.run(...values); 197 - 198 - if (info.changes === 0) { 199 - return c.json({ error: 'Not found' }, 404); 200 - } 201 - 202 - const getStmt = db.prepare(` 203 - SELECT t.*, c.name as category_name, c.color as category_color 204 - FROM todos t 205 - LEFT JOIN categories c ON t.category_id = c.id 206 - WHERE t.id = ? 207 - `); 208 - const todo = getStmt.get(id); 209 - 210 - return c.json(todo); 211 - }); 212 - 213 - router.delete('/todos/:id', (c) => { 214 - const id = parseInt(c.req.param('id')); 215 - if (isNaN(id)) { 216 - return c.json({ error: 'Invalid ID' }, 400); 217 - } 218 - 219 - const stmt = db.prepare('DELETE FROM todos WHERE id = ?'); 220 - const info = stmt.run(id); 221 - 222 - if (info.changes === 0) { 223 - return c.json({ error: 'Not found' }, 404); 224 - } 225 - 226 - return c.body(null, 204); 227 - }); 228 - 229 - // Stats route 230 - router.get('/stats', (c) => { 231 - const stmt = db.prepare(` 232 - SELECT 233 - COUNT(*) as total_todos, 234 - SUM(completed) as completed_todos, 235 - COUNT(*) - SUM(completed) as incomplete_todos 236 - FROM todos 237 - `); 238 - const stats = stmt.get(); 239 - return c.json(stats); 240 - }); 241 - 242 - // Serve the web interface 243 - router.get('/', (c) => { 244 - const html = `<!DOCTYPE html> 245 - <html lang="en"> 246 - <head> 247 - <meta charset="UTF-8"> 248 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 249 - <title>Todos</title> 250 - <style> 251 - * { 252 - margin: 0; 253 - padding: 0; 254 - box-sizing: border-box; 255 - } 256 - 257 - body { 258 - font-family: system-ui, -apple-system, sans-serif; 259 - background-color: #f8fafc; 260 - color: #334155; 261 - line-height: 1.6; 262 - } 263 - 264 - .container { 265 - max-width: 640px; 266 - margin: 0 auto; 267 - padding: 2rem 1rem; 268 - } 269 - 270 - h1 { 271 - text-align: center; 272 - font-size: 2.5rem; 273 - font-weight: 700; 274 - color: #1e293b; 275 - margin-bottom: 2rem; 276 - } 277 - 278 - .stats { 279 - background: white; 280 - border-radius: 0.5rem; 281 - padding: 1.5rem; 282 - margin-bottom: 2rem; 283 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 284 - display: flex; 285 - justify-content: space-around; 286 - text-align: center; 287 - } 288 - 289 - .stat { 290 - display: flex; 291 - flex-direction: column; 292 - } 293 - 294 - .stat-number { 295 - font-size: 2rem; 296 - font-weight: 700; 297 - color: #3b82f6; 298 - } 299 - 300 - .stat-label { 301 - font-size: 0.875rem; 302 - color: #64748b; 303 - text-transform: uppercase; 304 - letter-spacing: 0.05em; 305 - } 306 - 307 - .section { 308 - background: white; 309 - border-radius: 0.5rem; 310 - padding: 1.5rem; 311 - margin-bottom: 2rem; 312 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 313 - } 314 - 315 - .section h2 { 316 - font-size: 1.25rem; 317 - font-weight: 600; 318 - margin-bottom: 1rem; 319 - color: #1e293b; 320 - } 321 - 322 - .form { 323 - display: flex; 324 - gap: 0.75rem; 325 - margin-bottom: 1rem; 326 - flex-wrap: wrap; 327 - } 328 - 329 - input, select, button { 330 - padding: 0.5rem 0.75rem; 331 - border: 1px solid #d1d5db; 332 - border-radius: 0.375rem; 333 - font-size: 0.875rem; 334 - } 335 - 336 - input[type="text"] { 337 - flex: 1; 338 - min-width: 200px; 339 - } 340 - 341 - input[type="color"] { 342 - width: 3rem; 343 - height: 2.5rem; 344 - padding: 0.25rem; 345 - cursor: pointer; 346 - } 347 - 348 - select { 349 - min-width: 120px; 350 - } 351 - 352 - button { 353 - background: #3b82f6; 354 - color: white; 355 - border: none; 356 - cursor: pointer; 357 - font-weight: 500; 358 - transition: background-color 0.2s; 359 - } 360 - 361 - button:hover { 362 - background: #2563eb; 363 - } 364 - 365 - button.danger { 366 - background: #ef4444; 367 - } 368 - 369 - button.danger:hover { 370 - background: #dc2626; 371 - } 372 - 373 - button.small { 374 - padding: 0.25rem 0.5rem; 375 - font-size: 0.75rem; 376 - } 377 - 378 - .filters { 379 - display: flex; 380 - gap: 0.5rem; 381 - margin-bottom: 1rem; 382 - } 383 - 384 - .filter-btn { 385 - background: #f1f5f9; 386 - color: #475569; 387 - border: 1px solid #e2e8f0; 388 - padding: 0.5rem 1rem; 389 - border-radius: 0.375rem; 390 - cursor: pointer; 391 - font-size: 0.875rem; 392 - transition: all 0.2s; 393 - } 394 - 395 - .filter-btn.active { 396 - background: #3b82f6; 397 - color: white; 398 - border-color: #3b82f6; 399 - } 400 - 401 - .todo-list { 402 - list-style: none; 403 - } 404 - 405 - .todo-item { 406 - display: flex; 407 - align-items: center; 408 - gap: 0.75rem; 409 - padding: 0.75rem; 410 - border: 1px solid #e2e8f0; 411 - border-radius: 0.375rem; 412 - margin-bottom: 0.5rem; 413 - background: #fefefe; 414 - } 415 - 416 - .todo-checkbox { 417 - width: 1.25rem; 418 - height: 1.25rem; 419 - cursor: pointer; 420 - } 421 - 422 - .todo-title { 423 - flex: 1; 424 - font-weight: 500; 425 - } 426 - 427 - .todo-title.completed { 428 - text-decoration: line-through; 429 - color: #64748b; 430 - } 431 - 432 - .category-badge { 433 - padding: 0.25rem 0.5rem; 434 - border-radius: 0.25rem; 435 - font-size: 0.75rem; 436 - font-weight: 500; 437 - color: white; 438 - } 439 - 440 - .category-list { 441 - list-style: none; 442 - } 443 - 444 - .category-item { 445 - display: flex; 446 - align-items: center; 447 - gap: 0.75rem; 448 - padding: 0.5rem; 449 - border: 1px solid #e2e8f0; 450 - border-radius: 0.375rem; 451 - margin-bottom: 0.5rem; 452 - background: #fefefe; 453 - } 454 - 455 - .category-color { 456 - width: 1rem; 457 - height: 1rem; 458 - border-radius: 50%; 459 - } 460 - 461 - .category-name { 462 - flex: 1; 463 - font-weight: 500; 464 - } 465 - 466 - .empty-state { 467 - text-align: center; 468 - color: #64748b; 469 - font-style: italic; 470 - padding: 2rem; 471 - } 472 - </style> 473 - </head> 474 - <body> 475 - <div class="container"> 476 - <h1>todos</h1> 477 - 478 - <div class="stats"> 479 - <div class="stat"> 480 - <div class="stat-number" id="total-count">0</div> 481 - <div class="stat-label">Total</div> 482 - </div> 483 - <div class="stat"> 484 - <div class="stat-number" id="completed-count">0</div> 485 - <div class="stat-label">Completed</div> 486 - </div> 487 - <div class="stat"> 488 - <div class="stat-number" id="incomplete-count">0</div> 489 - <div class="stat-label">Incomplete</div> 490 - </div> 491 - </div> 492 - 493 - <div class="section"> 494 - <h2>Add Todo</h2> 495 - <div class="form"> 496 - <input type="text" id="todo-title" placeholder="Enter todo title..." /> 497 - <select id="todo-category"> 498 - <option value="">No category</option> 499 - </select> 500 - <button onclick="createTodo()">Add Todo</button> 501 - </div> 502 - </div> 503 - 504 - <div class="section"> 505 - <h2>Todos</h2> 506 - <div class="filters"> 507 - <button class="filter-btn active" onclick="setFilter('all')">All</button> 508 - <button class="filter-btn" onclick="setFilter('active')">Active</button> 509 - <button class="filter-btn" onclick="setFilter('completed')">Completed</button> 510 - </div> 511 - <ul class="todo-list" id="todo-list"> 512 - <li class="empty-state">Loading todos...</li> 513 - </ul> 514 - </div> 515 - 516 - <div class="section"> 517 - <h2>Categories</h2> 518 - <div class="form"> 519 - <input type="text" id="category-name" placeholder="Category name..." /> 520 - <input type="color" id="category-color" value="#3b82f6" /> 521 - <button onclick="createCategory()">Add Category</button> 522 - </div> 523 - <ul class="category-list" id="category-list"> 524 - <li class="empty-state">Loading categories...</li> 525 - </ul> 526 - </div> 527 - </div> 528 - 529 - <script> 530 - let currentFilter = 'all'; 531 - let todos = []; 532 - let categories = []; 533 - 534 - async function loadData() { 535 - await Promise.all([loadTodos(), loadCategories(), loadStats()]); 536 - renderTodos(); 537 - renderCategories(); 538 - } 539 - 540 - async function loadTodos() { 541 - const response = await fetch('/todos'); 542 - todos = await response.json(); 543 - } 544 - 545 - async function loadCategories() { 546 - const response = await fetch('/categories'); 547 - categories = await response.json(); 548 - renderCategorySelect(); 549 - } 550 - 551 - async function loadStats() { 552 - const response = await fetch('/stats'); 553 - const stats = await response.json(); 554 - document.getElementById('total-count').textContent = stats.total_todos; 555 - document.getElementById('completed-count').textContent = stats.completed_todos; 556 - document.getElementById('incomplete-count').textContent = stats.incomplete_todos; 557 - } 558 - 559 - function renderCategorySelect() { 560 - const select = document.getElementById('todo-category'); 561 - select.innerHTML = '<option value="">No category</option>'; 562 - categories.forEach(cat => { 563 - const option = document.createElement('option'); 564 - option.value = cat.id; 565 - option.textContent = cat.name; 566 - select.appendChild(option); 567 - }); 568 - } 569 - 570 - function renderTodos() { 571 - const list = document.getElementById('todo-list'); 572 - const filteredTodos = todos.filter(todo => { 573 - if (currentFilter === 'active') return !todo.completed; 574 - if (currentFilter === 'completed') return todo.completed; 575 - return true; 576 - }); 577 - 578 - if (filteredTodos.length === 0) { 579 - list.innerHTML = '<li class="empty-state">No todos found</li>'; 580 - return; 581 - } 582 - 583 - list.innerHTML = filteredTodos.map(todo => { 584 - const category = categories.find(c => c.id === todo.category_id); 585 - return \` 586 - <li class="todo-item"> 587 - <input type="checkbox" class="todo-checkbox" 588 - \${todo.completed ? 'checked' : ''} 589 - onchange="toggleTodo(\${todo.id})" /> 590 - <span class="todo-title \${todo.completed ? 'completed' : ''}">\${todo.title}</span> 591 - \${category ? \`<span class="category-badge" style="background-color: \${category.color}">\${category.name}</span>\` : ''} 592 - <button class="danger small" onclick="deleteTodo(\${todo.id})">Delete</button> 593 - </li> 594 - \`; 595 - }).join(''); 596 - } 597 - 598 - function renderCategories() { 599 - const list = document.getElementById('category-list'); 600 - 601 - if (categories.length === 0) { 602 - list.innerHTML = '<li class="empty-state">No categories found</li>'; 603 - return; 604 - } 605 - 606 - list.innerHTML = categories.map(category => { 607 - const todoCount = todos.filter(t => t.category_id === category.id).length; 608 - return \` 609 - <li class="category-item"> 610 - <div class="category-color" style="background-color: \${category.color}"></div> 611 - <span class="category-name">\${category.name}</span> 612 - \${todoCount === 0 ? \`<button class="danger small" onclick="deleteCategory(\${category.id})">Delete</button>\` : \`<span style="font-size: 0.75rem; color: #64748b;">(\${todoCount} todos)</span>\`} 613 - </li> 614 - \`; 615 - }).join(''); 616 - } 617 - 618 - function setFilter(filter) { 619 - currentFilter = filter; 620 - document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active')); 621 - event.target.classList.add('active'); 622 - renderTodos(); 623 - } 624 - 625 - async function createTodo() { 626 - const title = document.getElementById('todo-title').value.trim(); 627 - const categoryId = document.getElementById('todo-category').value; 628 - 629 - if (!title) return; 630 - 631 - const payload = { title }; 632 - if (categoryId) payload.category_id = parseInt(categoryId); 633 - 634 - const response = await fetch('/todos', { 635 - method: 'POST', 636 - headers: { 'Content-Type': 'application/json' }, 637 - body: JSON.stringify(payload) 638 - }); 639 - 640 - if (response.ok) { 641 - document.getElementById('todo-title').value = ''; 642 - document.getElementById('todo-category').value = ''; 643 - await loadData(); 644 - } 645 - } 646 - 647 - async function toggleTodo(id) { 648 - const todo = todos.find(t => t.id === id); 649 - const response = await fetch(\`/todos/\${id}\`, { 650 - method: 'PATCH', 651 - headers: { 'Content-Type': 'application/json' }, 652 - body: JSON.stringify({ completed: todo.completed ? 0 : 1 }) 653 - }); 654 - 655 - if (response.ok) { 656 - await loadData(); 657 - } 658 - } 659 - 660 - async function deleteTodo(id) { 661 - const response = await fetch(\`/todos/\${id}\`, { method: 'DELETE' }); 662 - if (response.ok) { 663 - await loadData(); 664 - } 665 - } 666 - 667 - async function createCategory() { 668 - const name = document.getElementById('category-name').value.trim(); 669 - const color = document.getElementById('category-color').value; 670 - 671 - if (!name) return; 672 - 673 - const response = await fetch('/categories', { 674 - method: 'POST', 675 - headers: { 'Content-Type': 'application/json' }, 676 - body: JSON.stringify({ name, color }) 677 - }); 678 - 679 - if (response.ok) { 680 - document.getElementById('category-name').value = ''; 681 - document.getElementById('category-color').value = '#3b82f6'; 682 - await loadData(); 683 - } 684 - } 685 - 686 - async function deleteCategory(id) { 687 - const response = await fetch(\`/categories/\${id}\`, { method: 'DELETE' }); 688 - if (response.ok) { 689 - await loadData(); 690 - } 691 - } 692 - 693 - // Initialize the app 694 - loadData(); 695 - </script> 696 - </body> 697 - </html>`; 698 - 699 - return c.html(html); 700 - }); 701 - 702 - export default router; 703 - 704 - /** @internal Phoenix VCS traceability — do not remove. */ 705 - export const _phoenix = { 706 - iu_id: '36212ecf5a73b3cdaf7f64d0fdfe77ac955188826af9854d63ab2168e88ab795', 707 - name: 'Web Interface', 708 - risk_tier: 'medium', 709 - canon_ids: [10 as const], 710 - } as const;
+26
examples/todo-app/src/server.ts
··· 1 + import { serve } from '@hono/node-server'; 2 + import { app, mount } from './app.js'; 3 + import { runMigrations } from './db.js'; 4 + 5 + // Generated route modules 6 + import data_integrity from './generated/todos/data-integrity.js'; 7 + import filtering_and_views from './generated/todos/filtering-and-views.js'; 8 + import integration from './generated/todos/integration.js'; 9 + import projects from './generated/todos/projects.js'; 10 + import quick_stats from './generated/todos/quick-stats.js'; 11 + import tasks from './generated/todos/tasks.js'; 12 + import web_experience from './generated/todos/web-experience.js'; 13 + 14 + // Mount routes 15 + mount('/data-integrity', data_integrity); 16 + mount('/filtering-and-views', filtering_and_views); 17 + mount('/integration', integration); 18 + mount('/projects', projects); 19 + mount('/quick-stats', quick_stats); 20 + mount('/tasks', tasks); 21 + mount('', web_experience); 22 + 23 + const port = parseInt(process.env.PORT ?? '3000', 10); 24 + runMigrations(); 25 + console.log(`Server running at http://localhost:${port}`); 26 + serve({ fetch: app.fetch, port });
+14 -1
src/architectures/sqlite-web-api.ts
··· 89 89 90 90 You are generating a route handler module for a Hono REST API backed by SQLite. 91 91 92 + ### Translating user requirements to implementation 93 + - The spec describes what USERS do, not API endpoints. YOU must derive the REST endpoints, database schema, and SQL queries from the user behaviors. 94 + - "Users can create X" → POST endpoint with validation + INSERT query 95 + - "Users can view X" → GET endpoint with SELECT query (consider JOINs for related data) 96 + - "Users can edit X" → PATCH endpoint with UPDATE query 97 + - "Users can delete X" → DELETE endpoint with safety checks 98 + - "Users can filter by Y" → query parameters on GET endpoints (?status=active&priority=urgent) 99 + - "Show X sorted by Y" → ORDER BY clause in SQL 100 + - "X must be visually highlighted" → this is a UI concern handled by the web interface module, not the API 101 + - "Expose a programmatic interface" → this is what you're building: the REST API IS the programmatic interface 102 + 92 103 ### CRITICAL import rules — follow EXACTLY, no exceptions 93 104 94 105 Your file MUST start with these EXACT three import lines: ··· 340 351 4. Query parameter filtering with dynamic WHERE clause building 341 352 5. Foreign key validation: check referenced row exists before INSERT 342 353 6. Cascade protection: check for dependent rows before DELETE of parent resource 343 - 7. Zod schemas for create/update validation 354 + 7. Zod schemas for create/update validation — use z.enum() for fixed sets like priority levels 344 355 8. Return the created/updated resource with JOINed data after mutation 345 356 9. Export default router + export _phoenix metadata 357 + 10. For sorting by priority/urgency, use a CASE expression: ORDER BY CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 END 358 + 11. For "overdue" logic: WHERE due_date < date('now') AND completed = 0 346 359 `; 347 360 348 361 // ─── Architecture definition ────────────────────────────────────────────────
+11
src/cli.ts
··· 990 990 } 991 991 console.log(); 992 992 993 + // Load architecture 994 + const configPath = join(phoenixDir, 'config.json'); 995 + let regenArch: Architecture | null = null; 996 + if (existsSync(configPath)) { 997 + try { 998 + const cfg = JSON.parse(readFileSync(configPath, 'utf8')); 999 + if (cfg.architecture) regenArch = getArchitecture(cfg.architecture); 1000 + } catch { /* ignore */ } 1001 + } 1002 + 993 1003 const regenCtx: RegenContext = { 994 1004 llm: llm ?? undefined, 995 1005 canonNodes, 996 1006 allIUs: ius, 997 1007 projectRoot, 1008 + architecture: regenArch, 998 1009 onProgress: (iu, status, msg) => { 999 1010 if (status === 'start') process.stdout.write(` ⏳ ${iu.name}…`); 1000 1011 else if (status === 'done') process.stdout.write(` ${green('✔')}\n`);