From a3958e8ce93c0a935197fc1612d4e2a47f9b290a Mon Sep 17 00:00:00 2001 From: Saivijaykurakula01 Date: Sat, 7 Feb 2026 20:02:03 +0000 Subject: [PATCH] implemented a small spec-driven full-stack example --- README.md | 127 +++++++++----- backend/__init__.py | 1 + backend/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 152 bytes backend/__pycache__/main.cpython-312.pyc | Bin 0 -> 1123 bytes backend/__pycache__/models.cpython-312.pyc | Bin 0 -> 1645 bytes backend/__pycache__/routes.cpython-312.pyc | Bin 0 -> 2072 bytes backend/__pycache__/storage.cpython-312.pyc | Bin 0 -> 2653 bytes backend/main.py | 29 ++++ backend/models.py | 31 ++++ backend/routes.py | 38 ++++ backend/storage.py | 45 +++++ frontend/index.html | 162 ++++++++++++++++++ requirements.txt | 6 + .../test_tasks.cpython-312-pytest-9.0.1.pyc | Bin 0 -> 10417 bytes .../test_tasks.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 10417 bytes tests/test_tasks.py | 63 +++++++ 16 files changed, 459 insertions(+), 43 deletions(-) create mode 100644 backend/__init__.py create mode 100644 backend/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/__pycache__/main.cpython-312.pyc create mode 100644 backend/__pycache__/models.cpython-312.pyc create mode 100644 backend/__pycache__/routes.cpython-312.pyc create mode 100644 backend/__pycache__/storage.cpython-312.pyc create mode 100644 backend/main.py create mode 100644 backend/models.py create mode 100644 backend/routes.py create mode 100644 backend/storage.py create mode 100644 frontend/index.html create mode 100644 requirements.txt create mode 100644 tests/__pycache__/test_tasks.cpython-312-pytest-9.0.1.pyc create mode 100644 tests/__pycache__/test_tasks.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/test_tasks.py diff --git a/README.md b/README.md index 494f1c75..7e4d5e2a 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,84 @@ -# Candidate Assessment: Spec-Driven Development With Codegen Tools - -This assessment evaluates how you use modern code generation tools (for example `5.2-Codex`, `Claude`, `Copilot`, and similar) to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both. - -## Goals -- Build a working application with at least one meaningful feature. -- Create a testing framework to validate the application. -- Demonstrate effective use of code generation tools to accelerate delivery. -- Show clear, maintainable engineering practices. - -## Deliverables -- Application source code in this repository. -- A test suite and test harness that can be run locally. -- Documentation that explains how to run the app and the tests. - -## Scope Options -Pick one: -- Frontend-only application. -- Backend-only application. -- Full-stack application. - -Your solution should include at least one real workflow, for example: -- Create and view a resource. -- Search or filter data. -- Persist data in memory or storage. - -## Rules -- You must use a code generation tool (for example `5.2-Codex`, `Claude`, or similar). You can use multiple tools. -- You must build the application and a testing framework for it. -- The application and tests must run locally. -- Do not include secrets or credentials in this repository. - -## Evaluation Criteria -- Working product: Does the app do what it claims? -- Test coverage: Do tests cover key workflows and edge cases? -- Engineering quality: Clarity, structure, and maintainability. -- Use of codegen: How effectively you used tools to accelerate work. -- Documentation: Clear setup and run instructions. - -## What to Submit -- When you are complete, put up a Pull Request against this repository with your changes. -- A short summary of your approach and tools used in your PR submission -- Any additional information or approach that helped you. +# Spec-Driven Task App + +This repository contains a small spec-driven full-stack example: a FastAPI backend and a plain HTML + JavaScript frontend for managing tasks in memory. + +Project structure +``` +backend/ + main.py + models.py + routes.py + storage.py +frontend/ + index.html +tests/ + test_tasks.py +requirements.txt +README.md +``` + +Quickstart + +1. Install dependencies (recommended in a virtualenv): + +```bash +pip install -r requirements.txt +``` + +2. Run the backend: + +```bash +python -m backend.main +``` + +The API will be available at `http://127.0.0.1:8000` and the OpenAPI docs at `http://127.0.0.1:8000/docs`. + +# Spec-Driven Task Manager + +This repository contains a small full-stack Task Manager implemented using a spec-driven approach. + +Task spec (single source of truth): + +{ + "id": "uuid", + "title": "string", + "description": "string", + "status": "pending | completed" +} + +## Run the backend + +Install dependencies into a virtualenv, then run with Uvicorn: + +```bash +python -m pip install -r requirements.txt +uvicorn backend.main:app --reload --port 8000 +``` + +The backend serves the API and the frontend. Open http://localhost:8000/ to use the web UI. + +API endpoints: +- `POST /tasks` — create a task (JSON: title, description) +- `GET /tasks` — list tasks +- `GET /tasks?status=pending|completed` — filter tasks by status +- `PATCH /tasks/{id}/status` — update a task's status (JSON: {status: "completed"}) + +Test-only endpoint: +- `POST /test/clear` — clears in-memory storage (used by the test suite) + +## Frontend + +Open http://localhost:8000/ in your browser. The single `index.html` page allows creating tasks, filtering, and toggling status. + +## Run tests + +```bash +python -m pip install -r requirements.txt +pytest -q +``` + +## How AI / Code generation tools were used + +- The project was implemented following a spec-driven workflow. Pydantic models were derived from the task spec and used for validation and OpenAPI generation. +- Code generation tools were used to scaffold boilerplate (models, routes, storage) and to iterate on API shapes and tests, accelerating implementation while preserving human review and refinement. + diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 00000000..2f5d08cb --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +# backend package diff --git a/backend/__pycache__/__init__.cpython-312.pyc b/backend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da0277e97a9ac3f96d627e326770590572d0547f GIT binary patch literal 152 zcmX@j%ge<81PN2xGlhWkV-N=&d}aZPOlPQM&}8&m$xy@uC%5dRfQL01Nw<*)4>6ffIP+ zrbmyO_Ta$-NA6zGG}??I!Fb}<1W%lN(`|tm56&iU-kbN{x8M6_J|_}!1ncJ84I{1~ z^h*Zq6=^w#FEB!H5JeQ5$ifATr5`dwmQql_SEy>LR=5zxD1;cMVX9Rll~B-K(3rNE zYD1pvEL;IpxiEr??!Z57X%x^D8l&-QM;ndCs+|?!DOZ!lX+{Y%X2lEfe=tf`Q|-U~ zC!fCHAEzyg)2?|;0}EcYyAlraFhM6xFI`564}mfnGf_wY_r&7a)BS-@d*~V33p4dq z)OPo3W;K)TKagP-i^wGL{Cm$Hf!i_T6W=IK8Yc4&q+D>Oai`=nUhbc9S#j(EHXU_G7(v3+%p{e2Vj{X zfUVlZVz4LgKYczkZBS~m6~b9L+Qe^i;__7pJepNSf|#bWqC4D}H*8OI2Vs#j%52{t zrq`~pn7`oA<8Xly3Y##Vr$uxwhR-v7U-03O`eyN=iJq!=pD8p7&EY(2Jl{`ISOg^#qBy1g^H7OSD@X8X0E zed}>e8~(d@zl~H^)Ada6*2Q}7V6e-e(SdO0H_~vZkw$9gX8Ef!@Iy`StEshz8&m63 YyItqr4{Z1B&~NHgBZAaHegTO80o)HO&j0`b literal 0 HcmV?d00001 diff --git a/backend/__pycache__/models.cpython-312.pyc b/backend/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7789827fe67032a0e0c46ce4ab7fee8e9971ed50 GIT binary patch literal 1645 zcmaKs&2Q936u@Wf@#k(5QW6TZP|-vP7f23O8!91$`a$}+m8$0A1D4qtQ7g9B%y_Bx z7Ac_aF`VGYHG*^hiC$Wf!`4#NLr>f)?N*gI^}X@tV^@edym|9x-kbM+^X%VdW_$u; z?a41mY7p`_E)Iq@R))WV@|bYKX-YC0Qi{5f8krfIlo({4aPtA-Owea$?GrL>wZLi% zc4)P<)d8z}%xXh4ua2BKp`)Yw5Zw*k`Ucoeys)jpPk)te3G zUSdK4t-McTCBEx$k;20HHYtUS({`P0Eyt;o;rp)f9%mwoDnE*{4)3O@H>2oYH%`YprYdDM z7e#TNcgh$&RTS-#Atv?eqt%}~a#Iy?Tc}l4i1t-3llvmS%Ef(=c8W~o{03gZlR$jZ46{#o%uWSW4yQ&q~)9GS;5~)arVk-Luw4p$|Itc>c zxsC1hLG$GA3m^Q^yu8OQ*R|lOz>ru$W!*|d$^n)8y?*~V)zP8b*v7_JpHGyG*Wwu7 z2J!lm4I-duZa!zv;Jd^T+QI<<76RaQ=n79bV&yqY!MSi#ZNjhMvh;(6+Z=*Pc0)w=`cj}?v#XU{#ENbX0HBIgQ4ldF2&0O&U z^)U$K+T8x~&-qh3>gm0``C#jd{)aDEFu1U^_07)dCrgjcyjeX=;R8NJs3WJF*Z%^Gxh=ghxwWLX#RKOhP~R{#J2 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/routes.cpython-312.pyc b/backend/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1ec7e0549e856ebbd14dd8e4d59b50b72d8c1da GIT binary patch literal 2072 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 literal 0 HcmV?d00001 diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 00000000..d6dc3b6d --- /dev/null +++ b/backend/main.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from .routes import router + +app = FastAPI(title="Spec-Driven Task Manager", version="0.1") +app.include_router(router) + +# Serve the frontend folder at the app root (index.html will be served) +app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend") +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from .routes import router + +app = FastAPI(title="Task API") +app.include_router(router) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 00000000..644236c6 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import Literal + + +class TaskBase(BaseModel): + title: str + description: str + + +class TaskCreate(TaskBase): + pass + + +class Task(TaskBase): + id: str + status: Literal["pending", "completed"] +from pydantic import BaseModel, Field +from typing import Optional, Literal +from uuid import UUID + + +class Task(BaseModel): + id: UUID + title: str = Field(..., min_length=1) + description: Optional[str] = "" + status: Literal["pending", "completed"] = "pending" + + +class TaskCreate(BaseModel): + title: str = Field(..., min_length=1) + description: Optional[str] = "" diff --git a/backend/routes.py b/backend/routes.py new file mode 100644 index 00000000..f3914f02 --- /dev/null +++ b/backend/routes.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException +from typing import List, Optional + +from . import storage +from .models import TaskCreate, Task + +router = APIRouter() + + +@router.post("/tasks", response_model=Task, status_code=201) +def create_task(payload: TaskCreate): + task = storage.create_task(payload.title, payload.description) + return task + + +@router.get("/tasks", response_model=List[Task]) +def get_tasks(status: Optional[str] = None): + if status and status not in ("pending", "completed"): + raise HTTPException(status_code=400, detail="invalid status") + return storage.list_tasks(status) + + +@router.patch("/tasks/{task_id}/status", response_model=Task) +def patch_status(task_id: str, payload: dict): + status = payload.get("status") + if status not in ("pending", "completed"): + raise HTTPException(status_code=400, detail="invalid status") + task = storage.update_status(task_id, status) + if not task: + raise HTTPException(status_code=404, detail="not found") + return task + + +# Test-only convenience endpoint to reset in-memory state +@router.post("/test/clear", status_code=204) +def test_clear(): + storage.clear_storage() + return diff --git a/backend/storage.py b/backend/storage.py new file mode 100644 index 00000000..7f6a6df7 --- /dev/null +++ b/backend/storage.py @@ -0,0 +1,45 @@ +"""In-memory, thread-safe storage for tasks. + +This module stores tasks as dictionaries with string UUID ids. It provides +thread-safe functions matching the API expectations used in the tests and +routes: `create_task`, `list_tasks`, `update_status`, and `clear_storage`. +""" +from typing import Dict, List, Optional +import threading +import uuid + +_lock = threading.Lock() +_tasks: Dict[str, Dict] = {} + + +def clear_storage() -> None: + with _lock: + _tasks.clear() + + +def create_task(title: str, description: str) -> Dict: + with _lock: + task_id = str(uuid.uuid4()) + task = {"id": task_id, "title": title, "description": description, "status": "pending"} + _tasks[task_id] = task + return task.copy() + + +def list_tasks(status: Optional[str] = None) -> List[Dict]: + with _lock: + items = list(_tasks.values()) + if status: + items = [t.copy() for t in items if t["status"] == status] + else: + items = [t.copy() for t in items] + return items + + +def update_status(task_id: str, status: str) -> Optional[Dict]: + if status not in ("pending", "completed"): + raise ValueError("invalid status") + with _lock: + if task_id not in _tasks: + return None + _tasks[task_id]["status"] = status + return _tasks[task_id].copy() diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..47c36f0d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,162 @@ + + + + + + Spec-Driven Task Manager + + + +

Tasks

+ +
+ + + +
+ + + +
+ + + + + + + + + + Task App + + + +

Tasks

+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2cd04045 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.95.0 +uvicorn>=0.22.0 +pytest>=7.0.0 +fastapi>=0.95.0 +uvicorn[standard]>=0.22.0 +pytest>=7.0.0 diff --git a/tests/__pycache__/test_tasks.cpython-312-pytest-9.0.1.pyc b/tests/__pycache__/test_tasks.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..471d603f6cfb113b3bf927e7228d845ed40cce39 GIT binary patch literal 10417 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 literal 0 HcmV?d00001 diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 00000000..29fb9156 --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,63 @@ +import sys +import os +import pytest + +# Ensure repository root is on sys.path so `backend` package is importable during tests +root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if root not in sys.path: + sys.path.insert(0, root) + +from fastapi.testclient import TestClient +from backend.main import app + + +client = TestClient(app) + + +def setup_function(): + # Clear shared in-memory state via test API + client.post("/test/clear") + + +def test_create_task(): + res = client.post("/tasks", json={"title": "Hello", "description": "World"}) + assert res.status_code == 201 + data = res.json() + assert data["title"] == "Hello" + assert data["description"] == "World" + assert data["status"] == "pending" + + +def test_list_tasks(): + client.post("/tasks", json={"title": "T1", "description": "D1"}) + client.post("/tasks", json={"title": "T2", "description": "D2"}) + res = client.get("/tasks") + assert res.status_code == 200 + data = res.json() + assert isinstance(data, list) + assert len(data) == 2 + + +def test_filter_by_status(): + r1 = client.post("/tasks", json={"title": "A", "description": "a"}).json() + r2 = client.post("/tasks", json={"title": "B", "description": "b"}).json() + # Set one task to completed via API + client.patch(f"/tasks/{r2['id']}/status", json={"status": "completed"}) + res_pending = client.get("/tasks", params={"status": "pending"}) + res_completed = client.get("/tasks", params={"status": "completed"}) + assert all(t["status"] == "pending" for t in res_pending.json()) + assert all(t["status"] == "completed" for t in res_completed.json()) + + +def test_update_status_endpoint(): + r = client.post("/tasks", json={"title": "X", "description": "x"}).json() + res = client.patch(f"/tasks/{r['id']}/status", json={"status": "completed"}) + assert res.status_code == 200 + assert res.json()["status"] == "completed" + + +def test_invalid_input(): + # missing title should return 422 + res = client.post("/tasks", json={"description": "no title"}) + assert res.status_code == 422 +