Developer Guide
Architecture Overview
External Nginx (optional)
strips /logictranslation/
│
▼
┌─────────────────────────────────────────────────────────┐
│ Container (:3000) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Internal Nginx (:3000) │ │
│ │ /api/* → Backend (127.0.0.1:8000) │ │
│ │ /* → Frontend (127.0.0.1:3001) │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌───────────────────┐ ┌────────────────────────────┐ │
│ │ Frontend (:3001) │ │ Backend (:8000) │ │
│ │ Next.js 14 (SSR) │ │ FastAPI (async) │ │
│ │ React 18 + TS │ │ SQLAlchemy + Pydantic │ │
│ │ TailwindCSS │ │ Z3 SMT Solver │ │
│ │ KaTeX rendering │ │ lark parser │ │
│ └───────────────────┘ └────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ PostgreSQL 15 (:5432) │
│ JSONB for flexible content │
└─────────────────────────────────────────────────────────┘
Local Development Setup
Prerequisites
- Docker & Docker Compose
- Node.js 18+ (for frontend tooling outside Docker)
Start in dev mode
git clone <repo-url>
cd Logic-Translation
docker compose up -d
This mounts the source code as volumes and enables hot-reload for both services:
- Frontend: http://localhost:3000 (Next.js dev server with HMR)
- Backend: http://localhost:8000 (uvicorn with
--reload) - API docs: http://localhost:8000/docs (Swagger UI)
TypeScript checking
cd frontend && npx tsc --noEmit
Backend Architecture
Directory Structure
backend/
├── main.py # FastAPI app instance, CORS, router
├── requirements.txt # Python dependencies
├── alembic/ # Database migrations
│ └── versions/ # Migration scripts
├── alembic.ini # Alembic configuration
├── scripts/
│ ├── init_admin.py # First-time admin creation
│ └── reset_password.py # CLI password reset
└── app/
├── api/
│ ├── router.py # Route registration
│ ├── auth.py # Login, register, change-password
│ ├── admin.py # User CRUD, role management
│ ├── deps.py # Auth dependencies (get_current_user, etc.)
│ ├── submissions.py # CRUD for submissions
│ └── validity.py # Parse formulas + Z3 validity check
├── core/
│ └── security.py # JWT, bcrypt, token creation
├── db/
│ └── session.py # SQLAlchemy async engine + session
├── logic/
│ ├── latex_parser.py # Recursive-descent parser for LaTeX → SMT-LIB
│ └── z3_checker.py # Z3 solver wrapper
├── models/
│ └── models.py # SQLAlchemy ORM models (User, Submission)
└── schemas/
└── schemas.py # Pydantic V2 schemas (request/response)
Key Design Decisions
JSONB Content Model
Submissions use PostgreSQL JSONB columns for content and review. This allows flexible, schema-evolving data without database migrations for every field change.
# Pydantic schemas use extra="allow" to preserve unknown fields
class SubmissionContentSchema(BaseModel):
original: Optional[OriginalProblemSchema] = ...
formalization: Optional[FormalizationSchema] = ...
llmChallenge: Optional[LlmChallengeSchema] = None
model_config = ConfigDict(extra="allow")
Review History Archival
When a teacher submits a new review, the previous review is automatically archived:
{
"review": {
"current": { "reviewText": "...", "passed": "good", ... },
"history": [
{ "reviewText": "...", "passed": "pending", "contentSnapshot": {...} }
]
}
}
LaTeX Parser (latex_parser.py)
A custom recursive-descent parser (not using lark grammar) that:
- Normalizes LaTeX input (e.g.,
∧→\wedge,->→\rightarrow) - Validates well-formed formulas (WFF)
- Generates SMT-LIB code for Z3
- Supports both propositional and first-order logic
- Handles juxtaposition notation (e.g.,
Px→P(x)) - Enforces arity consistency for predicates
Access Control
Student → can only CRUD their own submissions
Teacher → can read all submissions, write reviews
Admin → full access to everything + user management
Adding a New API Endpoint
- Create or edit a file in
backend/app/api/ - Register the router in
backend/app/api/router.py:from .your_module import router as your_router api_router.include_router(your_router, prefix="/your-prefix", tags=["your-tag"]) - Add Pydantic schemas in
backend/app/schemas/schemas.py - Use auth dependencies:
from .deps import get_current_active_user, get_current_teacher_user, get_current_admin_user
Database Migrations
# Generate a new migration after model changes
docker exec -w /app/backend logic_backend alembic revision --autogenerate -m "description"
# Apply migrations
docker exec -w /app/backend logic_backend alembic upgrade head
# Rollback one step
docker exec -w /app/backend logic_backend alembic downgrade -1
Frontend Architecture
Directory Structure
frontend/src/
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout (Navbar, Providers)
│ ├── page.tsx # Landing/redirect page
│ ├── login/page.tsx # Login form
│ ├── student/page.tsx # Student submission workspace
│ ├── teacher/page.tsx # Teacher review panel
│ ├── admin/page.tsx # Admin user management
│ └── change-password/page.tsx # Password change form
├── components/
│ ├── Navbar.tsx # Navigation bar (role-aware)
│ ├── LatexRenderer.tsx # KaTeX rendering component
│ └── ClientProviders.tsx # Auth + i18n context wrapper
└── utils/
├── api.ts # API base URL + login helper
├── auth.tsx # AuthProvider + useAuth hook
├── i18n.ts # Translation strings (EN/ZH)
├── i18nContext.tsx # I18nProvider + useI18n hook
└── latexParser.ts # Frontend LaTeX normalization
Key Patterns
Auth Hydration Safety
The auth system initializes synchronously from localStorage to avoid SSR/CSR hydration mismatches:
function getInitialAuth(): AuthState {
if (typeof window === "undefined") return { token: null, role: null, isLoggedIn: false };
const token = localStorage.getItem("token");
// ...
}
All pages guard with initialized:
if (!initialized || !isLoggedIn) return null;
Change Detection
The student page uses a snapshot-based dirty check:
const savedSnapshot = useRef<string>("");
const hasChanges = () => JSON.stringify(currentState) !== savedSnapshot.current;
i18n
All UI text lives in utils/i18n.ts as a flat key-value object with en and zh variants. Add new labels to both language objects.
Adding a New Page
- Create
frontend/src/app/your-page/page.tsx - Add
"use client"directive at top - Use
useAuth()for authentication anduseI18n()for translations - Add the
initializedguard for hydration safety - Add nav link in
Navbar.tsxif needed - Add i18n labels in
utils/i18n.ts(both EN and ZH)
Testing
Manual Testing Workflow
- Start dev environment:
docker compose up -d - Log in as each role and test features
- Run TypeScript check:
cd frontend && npx tsc --noEmit - Check backend logs:
docker logs logic_backend
API Testing with curl
# Login
TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=YourPass123' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# List submissions
curl -s http://localhost:8000/submissions/my \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# Parse formulas
curl -s -X POST http://localhost:8000/validity/test-id/parse-formulas \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"premises":["P \\\\rightarrow Q","P"],"conclusion":"Q","is_propositional":true,"entails":true}'
Building the Production Image
We recommend providing two separate tags for users on Docker Hub: one for the root path (default) and one for your specific subpath (/logictranslation).
1. Build for Root Path (Default)
# Build the default image (no subpath)
docker build -t ganganray/logic-formalization-web:latest .
# Tag and push
docker tag ganganray/logic-formalization-web:latest ganganray/logic-formalization-web:<VERSION>
docker push ganganray/logic-formalization-web:latest
docker push ganganray/logic-formalization-web:<VERSION>
2. Build for Subpath
# Build with the subpath argument
docker build -t ganganray/logic-formalization-web:latest-subpath \
--build-arg NEXT_PUBLIC_BASE_PATH=/logictranslation \
.
# Tag and push
docker tag ganganray/logic-formalization-web:latest-subpath ganganray/logic-formalization-web:<VERSION>-subpath
docker push ganganray/logic-formalization-web:latest-subpath
docker push ganganray/logic-formalization-web:<VERSION>-subpath