Skip to content

Repository files navigation

BuildMesh — Multiplayer Software-Engineering Sprint Simulation

BuildMesh started as an offline, AI-assisted requirement-analysis and test-case-generation platform (Phases 0–3 below) and has since grown into a full multiplayer Scrum simulation game built on top of it: real users join a session, take on real Scrum roles (Scrum Master, Developer, Tester, plus an AI-controlled Product Owner), and run real sprints — planning, executing, facing simulated events and pressure, reviewing outcomes, holding retrospectives, and seeing their decisions measured across sprints — all against one real, persistent backlog, not a disconnected game state.

Fully offline and self-contained — no required external APIs (an optional local Ollama LLM can be enabled; everything has a deterministic rule-based fallback otherwise). JWT authentication, two independent role systems (account-level RBAC and in-session Scrum roles), SQLite, and a Next.js frontend.

What it solves: turns "read about Scrum" into "run a sprint and see what actually happens" — a team can practice real sprint planning, backlog grooming, QA, change management, and retrospectives with consequences (schedule pressure, defects, scope creep, risk) that are computed deterministically from what the team actually does, not scripted or random-for-its-own-sake.

Major functionality (see each phase's section below for detail): requirement/test-case authoring with an offline NLP pipeline (Phase 3); multiplayer sessions with join codes and Scrum roles (Phase 1); a real backlog — epics, stories, sprints, tasks, bugs, QA (Phase 2); an AI-driven Product Owner and change-request pipeline (Phase 4); live WebSocket updates (Phase 5); XP/achievements (Phase 6); a deterministic, replayable sprint simulation with events and risk (Phases 7–8); retrospectives with real follow-through (Phase 9); a multi-sprint history/trend view (Phase 10); and a sprint-planning assistant that surfaces a team's own velocity at the moment it plans its next sprint (Phase 11).

Getting Started

Everything below has been run and verified against a fresh clone of this exact repository — no step is invented.

Prerequisites: Python 3.9+, Node.js 18.17+ (required by Next.js 14), and git. No external services, API keys, or paid accounts are needed — BuildMesh is fully offline by default.

1. Clone the repository

git clone https://github.com/soyebmohammad03-dev/BuildMesh.git
cd BuildMesh

2. Enter the repository directory

Already done by cd BuildMesh above. All commands below assume you are in this root directory (the one containing main.py, requirements.txt, and the frontend/ folder) unless a step says otherwise.

3. Backend setup

The backend lives at the repository root (api/, core/, db/, engines/, main.py) — there is no separate backend/ folder.

python3 -m venv venv
source venv/bin/activate       # Windows: venv\Scripts\activate
pip install -r requirements.txt

4. Frontend setup

The frontend is the Next.js app in frontend/.

cd frontend
npm install
cd ..

5. Environment variables

Copy the example files and adjust if needed — the defaults below are enough to run everything locally with no changes.

cp .env.example .env
cp frontend/.env.example frontend/.env.local
Variable Where Required locally? Purpose
SECRET_KEY .env Optional locally JWT signing key. Falls back to a development default if unset (a startup warning is only logged when DEBUG=false). Must be set to a real random value for any non-local deployment.
DEBUG .env Optional (default false) Set to true for backend auto-reload during local development.
NEXT_PUBLIC_API_URL frontend/.env.local Yes The backend URL the frontend calls. Defaults to http://127.0.0.1:8001, which matches the backend command below — leave it as-is for local use.
CORS_ALLOWED_ORIGINS .env Optional Comma-separated allowed frontend origins. Unset, the backend already accepts the default local frontend origins (localhost:3000/3002), so this is not needed for local development.
BUILDMESH_DB_URL_OVERRIDE .env Optional Relocates the SQLite database file. Leave unset to use the default data/requirements.db.
PORT .env Optional (default 8001) Backend port when started via python3 main.py.
ADMIN_USERNAME / ADMIN_EMAIL / ADMIN_PASSWORD .env Optional Overrides for the admin-seeding step below. Leave unset locally to get admin / admin123.
LLM_ENABLED / LLM_PROVIDER / OLLAMA_* .env Optional Enables an optional local Ollama LLM (Phase 3). Everything works with deterministic fallback logic if left at the defaults — no LLM installation is required to run or test BuildMesh.

None of these need to contain a real secret to run BuildMesh locally — the placeholder values in .env.example are sufficient.

6. Database setup / migrations

alembic upgrade head

This creates data/requirements.db (SQLite) with the full current schema if it doesn't exist yet, or brings an existing one up to date. See Database Migrations further below for how schema changes are made after this point.

7. Seed the admin account (optional, recommended)

python scripts/seed_admin.py

Creates an admin / admin123 account (or your ADMIN_* overrides from step 5) so you have an account to log in with immediately. This step is optional — you can also just register a normal account through the app itself — but an admin account is needed to create the first session as its host.

8. Start the backend

Terminal 1:

python3 main.py

This is the real entry point (main.py) and already sets the --ws websockets-sansio flag the real-time layer requires — see Real-Time Layer (Phase 5) below for why that flag matters. Runs on http://127.0.0.1:8001 by default.

(Equivalent alternative, e.g. for --reload during active backend development: python3 -m uvicorn api.main:app --reload --port 8001 --ws websockets-sansio.)

9. Start the frontend

Terminal 2 (leave Terminal 1 running):

cd frontend
npm run dev:frontend

Runs on http://localhost:3002.

10. Open the application

Visit http://localhost:3002/login in your browser.

11. Local development credentials

If you ran the admin-seeding step: admin / admin123.

This is a well-known local-development default, not a production credential — scripts/seed_admin.py prints a warning if you use it, and you should override it with ADMIN_PASSWORD (step 5) for anything beyond your own machine. You can also skip seeding entirely and register your own account from the login page (self-registration can create VIEWER, QA_ENGINEER, or REQUIREMENT_ANALYST accounts — see Security notes below).

12. Troubleshooting

  • ModuleNotFoundError / import errors on backend start — make sure the virtual environment is activated (source venv/bin/activate) and pip install -r requirements.txt completed without errors.
  • Frontend can't reach the backend / requests fail — confirm the backend is actually running on port 8001 (curl http://127.0.0.1:8001/health) and that frontend/.env.local has NEXT_PUBLIC_API_URL=http://127.0.0.1:8001.
  • WebSocket/real-time updates don't work, or the backend crashes on a disconnect — you're almost certainly missing --ws websockets-sansio. python3 main.py already sets this; a manual uvicorn invocation must set it explicitly (step 8's alternative command shows the flag).
  • alembic upgrade head complains tables already exist — this usually means the app was started once before migrations were ever run against a brand-new database file. See Database Migrations below for why ordering matters here.
  • Port already in use — another process is already using 8001 or 3002. Stop it, or override the backend port with PORT=<other-port> (step 5) and update NEXT_PUBLIC_API_URL to match; the frontend port can be changed by editing the dev:frontend/start scripts in frontend/package.json.
  • Login works but nothing loads on a session page — check the browser console and the backend terminal for errors; every session page needs an active WebSocket connection in addition to the REST API, so confirm both servers are actually running.

Architecture

BuildMesh/
├── api/
│   ├── main.py              # FastAPI app entry
│   ├── auth.py              # Registration, login, JWT, logout
│   ├── dependencies.py      # get_current_user, auth dependencies
│   └── routes/
│       ├── auth_routes.py       # POST /api/auth/register, login, logout, me
│       ├── requirement_routes.py # Upload SRS, list, edit requirements
│       ├── testcase_routes.py   # Generate, list, edit test cases
│       ├── dashboard_routes.py  # Metrics, projects
│       ├── feedback_routes.py   # Submit corrections, stats
│       ├── admin_routes.py      # User management (Admin only)
│       └── ws_routes.py         # Phase 5: session-scoped WebSocket channel
│
├── core/
│   ├── security.py          # bcrypt, JWT
│   ├── rbac.py              # Role-based permissions
│   └── realtime/            # Phase 5: in-process connection manager + event model
│
├── db/
│   ├── database.py          # SQLite, SessionLocal
│   └── models.py            # Users, Projects, Requirements, TestCases, FeedbackLogs, TraceabilityMap
│
├── engines/                 # NLP engines (offline)
│   ├── requirement_processor.py
│   ├── test_case_generator.py
│   ├── ambiguity_detector.py
│   ├── traceability.py
│   └── feedback_learning.py
│
├── services/
│   └── document_parser.py   # Extract text from .txt, .docx, .pdf
│
├── static/
├── templates/
├── data/
└── evaluation/

Roles & Permissions (RBAC)

Role Permissions
ADMIN Full access, manage users, view analytics, delete projects
QA_ENGINEER Generate test cases, view dashboard, submit feedback, upload documents
REQUIREMENT_ANALYST Upload SRS, view ambiguity reports, edit requirements
VIEWER Read-only dashboard

Database Tables (SQLite)

  • users — id, username, email, hashed_password, role, created_at
  • projects — id, name, description, owner_id
  • requirements — id, requirement_id, project_id, raw_text, actors, actions, inputs, ...
  • test_cases — id, test_id, requirement_id, category, title, steps, expected_result, ...
  • feedback_logs — id, user_id, test_id, original/corrected fields
  • traceability_map — id, requirement_id, test_id, category, confidence
  • token_blacklist — logout token invalidation

Workflow

  1. Register — POST /api/auth/register (username, email, password, role) — self-registration can only grant VIEWER, QA_ENGINEER, or REQUIREMENT_ANALYST; see Security Notes below.
  2. Login — POST /api/auth/login → JWT token
  3. Create project — POST /api/dashboard/projects (name, description)
  4. Upload SRS — POST /api/requirements/upload (file: .txt/.docx/.pdf, project_id)
  5. Generate test cases — POST /api/testcases/generate?requirement_id=REQ-xxx
  6. View dashboard — GET /api/dashboard/metrics?project_id=1
  7. Edit test case — PUT /api/testcases/{test_id} (title, steps, expected_result)
  8. Submit feedback — POST /api/feedback/submit (original/corrected)

Auth

All protected endpoints require header: Authorization: Bearer <token>

Security notes

  • Self-registration cannot grant ADMIN. POST /api/auth/register accepts a role, but a request for ADMIN (in any casing) is rejected with 403; the role is otherwise restricted to VIEWER, QA_ENGINEER, or REQUIREMENT_ANALYST, falling back to VIEWER for anything unrecognized. This closes a privilege-escalation path where any unauthenticated caller could previously self-assign ADMIN. The intended way to create an ADMIN account is still python scripts/seed_admin.py (bootstrap) or POST /api/admin/users (by an existing admin) — both are unaffected.
  • Account role (ADMIN / QA_ENGINEER / REQUIREMENT_ANALYST / VIEWER) is a platform-level permission tier, deliberately kept separate from the future in-session game roles (Owner/AI, Scrum Master, Developer, Tester) described in the BuildMesh architecture blueprint — the two are not the same concept and will not be merged.

Database Migrations

Schema changes are managed with Alembic (alembic/), not Base.metadata.create_all() alone:

alembic current                                # show applied revision
alembic revision --autogenerate -m "add X"      # after changing db/models.py
alembic upgrade head                            # apply pending migrations

Base.metadata.create_all() (called once at app startup via init_db()) is left in place as a convenience for a completely fresh checkout with no database file yet — it is idempotent and never alters existing tables. Any change to an existing table must go through a migration, since create_all() cannot do that. Always run alembic upgrade head before first starting the app against a brand-new database file: if the app starts first, create_all() will create the current schema directly without stamping an Alembic revision, and a later alembic upgrade head against that same file will then fail trying to re-create tables that already exist.

Multiplayer Sessions (Phase 1)

The game layer's foundation: a GameSession is the "room" a join code admits players to, independent of (and simpler than) the account-level auth system above. POST /api/sessions creates one (real host, real join code via core/join_codes.py); POST /api/sessions/join (join code) and POST /api/sessions/{id}/role (pick SCRUM_MASTER / DEVELOPER / TESTER — OWNER is AI-controlled, see Phase 4) get a user into it. SESSION_ROLE_CAPACITY caps how many players can hold each role at once.

Two roles, deliberately never merged: a user's account-level UserRole (ADMIN/QA_ENGINEER/REQUIREMENT_ANALYST/VIEWER, from the original platform above) and their in-session SessionRole are independent concepts, checked by entirely separate authorization code paths (core/rbac.py vs. api/session_deps.py) — an account being ADMIN never grants any session-scoped privilege, and every phase built since has been tested to confirm this boundary holds (see each phase's authorization tests).

Real Backlog & Sprint Workflow (Phase 2)

A real Scrum backlog scoped to one session: Epics → User Stories → Tasks, plus Bugs and Test Executions, all in api/routes/backlog_routes.py and qa_routes.py. Sprints move through PLANNED → ACTIVE → CLOSED (POST .../sprints, .../start, .../close); stories move into a sprint explicitly (.../stories/{id}/move-to-sprint); tasks are assigned and tracked through real status transitions (TO_DO → IN_PROGRESS → READY_FOR_TEST → DONE, or BLOCKED). QA records real TestExecution rows against generated test cases and can file real Bugs from a failure. This backlog is the single real data source every later phase (simulation, metrics, retrospectives, the Project Arc) reads from — nothing in the game layer keeps a separate copy of "what work exists."

Local LLM / NLP Pipeline (Phase 3)

The original requirement-analysis/test-case-generation engines (engines/) gained an optional local LLM backend (core/llm/, Ollama only) alongside their original deterministic, rule-based logic. Every generation result is honestly labeled with its real generation_method (LLM_GENERATED or FALLBACK) — never presented as AI-generated when it wasn't. LLM_ENABLED=false is a hard kill switch forcing every engine onto its fallback (what the test suite uses, so unit tests never depend on a live model); LLM_PROVIDER/OLLAMA_BASE_URL/OLLAMA_MODEL/ LLM_TIMEOUT_SECONDS configure the provider when enabled (see .env.example). This same fallback-first design is what every later phase's AI-driven feature (the Owner below, in-sprint scope events) also relies on: the platform must produce a real, usable, clearly-labeled result whether or not Ollama is actually running.

AI Owner & Change Engine (Phase 4)

An AI-controlled OWNER session role (api/routes/owner_routes.py, engines/owner_engine.py) that plays the Product Owner: POST .../owner/generate-srs turns a one-line project idea into an initial set of real Requirement rows (via the Phase 3 pipeline, honestly labeled FALLBACK or LLM_GENERATED). Stakeholder change requests go through a real, auditable lifecycle — DRAFT → PROPOSED → ACCEPTED/REJECTED (ChangeRequest/ChangeRequestImpact) — where the Scrum Master, a real human, is the one who decides (.../accept / .../reject); accepting one mechanically applies its proposed actions to the real backlog (engines/change_engine.py: new/modified requirements, stories, tasks, priority changes — never a narrative-only effect). This propose/decide shape is the same pattern Phase 9's retrospective action items reuse.

Real-Time Layer (Phase 5)

A session-scoped WebSocket channel makes the BuildMesh lobby, backlog, task board, and AI Owner pages update live for every connected member, instead of relying only on polling. This is explicitly an in-process MVP, not a distributed real-time architecture — see "What's deferred" below.

How it works

  • One endpoint: GET /api/ws/sessions/{session_id} (an actual WebSocket upgrade, not REST). Connecting to it subscribes you to that session's events only — there is no "subscribe to everything" or cross-session channel.
  • core/realtime/manager.py's ConnectionManager is a single in-process, in-memory registry of session_id -> user_id -> live sockets. It is not backed by a database table and not shared across processes.
  • core/realtime/events.py defines a small, closed EventType enum and an emit(session_id, event_type, data, actor_user_id) helper. REST routes call emit() once, explicitly, right after their db.commit() succeeds — nothing is broadcast automatically from generic DB writes.
  • The database is always the source of truth. An event's data payload is a short summary (the affected id(s) and its new state) — clients are expected to refetch the authoritative REST resource for anything more detailed, not reconstruct state purely from the event stream.

Authentication and session isolation

The browser's native WebSocket API cannot set an Authorization header, and a token in the connection URL would end up in server/proxy access logs — so authentication happens over the socket itself:

  1. Client opens ws://.../api/ws/sessions/{id}.
  2. Server calls accept(), then waits (10s timeout) for exactly one JSON text frame: {"type": "auth", "token": "<jwt>"}.
  3. The token is validated with the exact same logic api/dependencies.py::get_current_user uses for REST (signature, expiry, blacklist, active user) — there is no second auth system.
  4. The resolved user must then have an active SessionMembership row for that specific session_id — the same model/table api/session_deps.py already uses for every Phase 1-4 REST route. Session/game role (SessionRole) is still completely independent from account-level UserRole/RBAC; the WebSocket layer checks the former only, exactly like REST.
  5. Anything else — session doesn't exist, auth times out or fails, or the user is authenticated but not a member of this session — closes the socket immediately with a distinct code so a client can tell them apart: 4404 (not found), 4401 (unauthenticated), 4403 (forbidden).

Because authorization is just "active membership in this session_id", one session's events are structurally unreachable from another session's connection — there is no cross-session broadcast path to accidentally hit.

Event types

core/realtime/events.py::EventType — one member per meaningful existing mutation, not "every write":

Event Emitted by
member_joined, member_left, role_changed session_routes.py
story_created, story_updated, story_moved backlog_routes.py
sprint_created, sprint_started, sprint_closed backlog_routes.py
task_created, task_assigned, task_status_changed backlog_routes.py
test_execution_recorded, bug_filed, bug_status_changed qa_routes.py
change_request_updated owner_routes.py (submitted/proposed/accepted/rejected — the status field in data distinguishes which)
presence_update ws_routes.py itself, on every connect/disconnect

Every message has the same envelope:

{"type": "story_created", "session_id": 6, "data": {"story": {...}}, "actor_user_id": 3, "ts": "2026-08-25T09:52:21.768239"}

Presence

"Online" is derived, not stored: it is just "has a live WebSocket connection right now" per ConnectionManager. The member roster and roles still come from the real SessionMembership rows. No new table exists for this, and no Redis/pub-sub was introduced — the brief for this phase explicitly asked for neither.

Frontend integration

frontend/lib/useSessionSocket.ts is a small hook: connect, send the auth frame, call the caller's onEvent(event) for every message, and auto-reconnect (3s backoff) on drop. It does not parse events into partial UI state — pages that care about a given event type just refetch their existing REST resource:

  • Lobby (sessions/[id]/page.tsx): renders a live online/offline dot per participant from presence_update; refetches the roster on member_joined/member_left/role_changed.
  • Backlog (sessions/[id]/backlog/page.tsx): refetches on story/sprint/task creation and movement events.
  • Board (sessions/[id]/board/page.tsx): refetches on task/QA/bug events.
  • AI Owner (sessions/[id]/owner/page.tsx): refetches on change_request_updated.

Polling still exists on every page as a fallback for a dropped/ reconnecting socket, just at a much longer interval (15-20s) than before Phase 5 — the WebSocket event is what makes the update feel immediate.

Testing this locally

# Terminal 1
python3 -m uvicorn api.main:app --port 8001 --ws websockets-sansio
# Terminal 2
npm --prefix frontend run dev:frontend

Open the same session in two browsers (or one browser + a real WebSocket client for the second participant — tests/test_realtime.py's live_server fixture does exactly this with the websockets Python package) and perform a backlog/task/QA action as one member; the other member's page updates without a manual refresh.

Automated tests: pytest tests/test_realtime.py -v — connection-lifecycle and authorization tests run against the in-process TestClient; tests that need one connection's mutation to reach another live connection run against a real uvicorn.Server on a loopback port (TestClient runs every call on its own separate event loop, so it cannot exercise cross-connection broadcast — see that file's module docstring for the full reasoning).

Known issue found during this phase (fixed)

The default uvicorn WebSocket implementation (--ws websockets, uvicorn's adapter over the now-deprecated websockets.legacy module) crashed the entire server process under a specific race: a client disconnecting at the same moment the server also decided to close the connection (e.g. an auth timeout) triggered a native-level fault after an initial RuntimeError ("got a second 'websocket.close'"). This was reproduced against the real dev server during manual E2E testing, not just theorized. Two independent fixes are in place: (1) ws_routes.py no longer calls close() after a WebSocketDisconnect, and every close() call is wrapped so a close-related exception can never propagate; (2) the app is run with --ws websockets-sansio, uvicorn's newer, non-legacy implementation, which does not exhibit this fault. Run uvicorn with --ws websockets-sansio in every environment, not just as a nice-to-have — see "Technical debt" in the Phase 5 report for more detail on what's still not verified about this dependency combination.

What's deferred

This is a single-process, in-memory implementation. Explicitly out of scope for this phase (see the Phase 5 completion report for the full list): horizontal scaling / multiple backend processes, Redis or any pub/sub broker, connection persistence across a process restart, event replay / guaranteed delivery, offline client queuing, richer presence (idle/away, typing indicators), and spectator/read-only connections. Running more than one API process today would mean a client connected to process A never sees an event caused by a request handled by process B — acceptable for the current single-process MVP/dev deployment, not for a horizontally-scaled one.

Metrics & Gamification (Phase 6)

Session/team metrics, an XP ledger, and a small achievement system — all derived from real backlog/QA/AI-Owner records, never from a separately tracked "score."

  • core/metrics/engine.py — read-only calculations: completion, velocity, burndown, defects, QA/test metrics, player contribution, team aggregate. Velocity uses UserStory.story_points when a session actually sets them, and always also reports a plain stories/tasks-completed count as an honest fallback for sessions that never set points. Burndown is approximated from each task's current status/updated_at — the schema has no persisted daily snapshot, so this is documented in the API response itself (burndown.limitation), not hidden.
  • core/metrics/xp_rules.py — the one central XP rule table (award_xp() is the only function that ever inserts a ledger row). db.models.XpLedgerEntry is append-only and idempotent by a DB unique constraint on (session_id, user_id, event_type, source_type, source_id) — the same real event can never pay out twice.
  • core/metrics/achievements.py — a small static catalog (7 achievements) with deterministic, DB-state-based unlock conditions; db.models.AchievementUnlock persists each unlock once per (session_id, user_id, achievement_key).
  • core/metrics/hooks.py — the only place that calls award_xp() / unlock_achievement(), one function per real action (task completed, story completed, test executed, bug reported/resolved, sprint closed, an AI Owner change request accepted). Route handlers in backlog_routes.py, qa_routes.py, and owner_routes.py each call exactly one hook, right after the state change that earned it.
  • Change-request XP is scaled by the real number of created/modified entities from engines/change_engine.py's application summary, not a flat reward for clicking Accept — see hooks.py::on_change_request_accepted.
  • GET /api/sessions/{id}/metrics, .../metrics/sprints/{sprint_id}, .../metrics/players, .../achievements — all read-only, gated by active session membership like every other Phase 2-5 route.
  • The dashboard (frontend/app/sessions/[id]/metrics/page.tsx) refetches on the relevant Phase 5 WebSocket event types (including two new ones, xp_awarded and achievement_unlocked, emitted only from core/metrics/hooks.py) rather than polling aggressively.

Sprint Simulation (Phase 7)

Turns a sprint from a static Scrum board into a playable, server- authoritative simulation — deterministic, replayable, and grounded in the same real backlog/QA/AI-Owner mechanics as every earlier phase.

  • core/simulation/events.py — 5 event types (PRODUCTION_BUG, BLOCKED_TASK, SCOPE_INCREASE, PRIORITY_CHANGE, RESOURCE_CONSTRAINT), each a real consequence on an existing entity (a real Bug, a real Task.status = BLOCKED, a real ChangeRequest routed through the unmodified Phase 4 AIOwnerEngine, a real UserStory.priority change, a real capacity deduction). Generation is random.Random(f"{sprint.rng_seed}:{turn}") — deterministic and replayable, never real randomness; rng_seed is fixed to the sprint's own id at start_sprint, never client-supplied.
  • core/simulation/engine.py — advance_turn() (the one new player action: increments Sprint.current_turn and maybe generates one event), capacity_metrics() (planned/completed/remaining points plus an effective-capacity figure that accounts for RESOURCE_CONSTRAINT events), and activity_feed() (a chronological merge of real timestamped records — never fabricated entries).
  • core/simulation/scoring.py — compute_sprint_outcome() combines Phase 6's existing metrics (never a second metrics system) into five named, weighted dimensions (delivery/quality/reliability/scope/process) plus one overall score, persisted once per sprint in SprintOutcome.
  • core/simulation/hooks.py — resolves an event only when its real underlying entity actually changes (a blocked task leaves BLOCKED, a bug reaches RESOLVED/CLOSED, a change request is decided) — a player can never simply declare an event resolved. Feeds a small outcome-scaled XP bonus and a "Crisis Manager" achievement into the existing Phase 6 XP ledger/achievement catalog — no parallel scoring system.
  • Two new endpoints: POST .../sprints/{id}/turns/advance (Scrum Master) and GET .../sprints/{id}/simulation (any member) — every other simulation action reuses an existing Phase 2/4 endpoint (task status, bug status, change-request accept/reject).
  • The dashboard (frontend/app/sessions/[id]/simulation/page.tsx) shows turn/status, capacity, pending/resolved events with resolution hints, the activity feed, and — once closed — the Sprint Review outcome, all refreshed live over the existing Phase 5 WebSocket layer.

Advanced Simulation, Analytics & Replay (Phase 8)

Layers a health/risk model, a persisted replay history, richer analytics, and two new event types onto Phase 7's simulation — reusing its engine, never a second simulation system.

  • core/simulation/events.py — two new event types, QUALITY_REGRESSION (eligible once a task's most recent TestExecution is a real FAIL; resolved only by a genuine subsequent PASS on the same task — no click-resolve shortcut) and STAKEHOLDER_ESCALATION (eligible once 3+ simulation events are simultaneously PENDING; auto-resolves the moment real player action drops the pending count back below that threshold). The flat Phase 7 EVENT_CHANCE_PER_TURN constant is replaced by event_chance(), a pure function of real DB pressure signals (unresolved events, blocked tasks, open bugs) built from named constants — still fully deterministic under the same seeded RNG.
  • core/simulation/health.py — a live, always-recomputable risk model across 5 named dimensions (delivery/quality/capacity/scope/process), distinct from scoring.py's once-at-close outcome: every formula reads real DB facts (completion %, QA pass rate, capacity reduction, pending vs. accepted scope events, unresolved-event ratio) through Phase 6's existing metrics engine, never a duplicate one.
  • core/simulation/history.py + the new SprintTurnSnapshot table — one immutable row per turn actually advanced (its capacity/health snapshot and which event, if any, it produced), the minimal real record a replay needs rather than a full DB snapshot. GET .../sprints/{id}/history is strictly read-only — replay can never mutate a sprint.
  • core/simulation/timeline.py — merges Phase 7's activity_feed() with change-request decisions, XP awards, and achievement unlocks from the sprint's real active window into one chronological record.
  • core/simulation/analytics.py — per-player/team simulation analytics (events resolved, by type, crisis-resolution counts, resolution share) layered on top of — never replacing — Phase 6's player_contribution().
  • Three new read-only endpoints: GET .../sprints/{id}/history, .../timeline, and .../analytics, plus one new realtime event, risk_changed, emitted alongside every turn advance over the existing Phase 5 WebSocket layer.
  • The simulation dashboard gained a Sprint Health risk-bar panel, an enriched Timeline card, and a read-only Turn History (Replay) stepper for stepping through a sprint's recorded turns.

Sprint Retrospectives & Continuous Improvement (Phase 9)

Closes the Scrum ceremony loop: Phases 1-8 already cover Planning, Execution, and Review (the persisted SprintOutcome — literally called "the Sprint Review" in simulation_routes.py's own docstring); Phase 9 adds the missing Retrospective, and makes its outcome a real, measurable consequence rather than a comment box.

  • core/retro/service.py — the real entry/action-item lifecycle: any active member submits a WENT_WELL / NEEDS_IMPROVEMENT / ACTION_ITEM entry against a sprint only once it's genuinely CLOSED; an ACTION_ITEM moves OPEN -> ADOPTED -> APPLIED (or DISMISSED), mirroring ChangeRequest's propose/decide shape. Entries are immutable once created, same audit-trail philosophy as Phase 8's turn snapshots.
  • Adoption is a Scrum-Master decision; auto-attachment binds every session-wide ADOPTED item to the next sprint the moment it actually starts (backlog_routes.py::start_sprint -> core/retro/hooks.py:: on_sprint_started) — never client-supplied. Applying an item is open to any active member, but only for the sprint it's attached to, and only while that sprint is genuinely ACTIVE.
  • core/simulation/health.py's live _process_risk() gets one small, named, capped reduction per real APPLIED action item on the current sprint (PROCESS_RISK_REDUCTION_PER_APPLIED_ACTION, capped by PROCESS_RISK_REDUCTION_CAP) — additive to the existing formula; the once-at-close scoring.py outcome is deliberately left untouched.
  • XP for submitting an entry, for an authored action item being adopted, and for applying one (core/metrics/xp_rules.py), plus one new achievement, Retro Champion, unlocked when a player's own action item reaches APPLIED — all through the existing Phase 6 ledger/catalog.
  • Two new realtime events, retro_entry_submitted and retro_action_status_changed, over the existing Phase 5 WebSocket layer.
  • New endpoints under api/routes/retro_routes.py: submit/list entries per sprint, adopt/dismiss/apply an action item, and a session-wide GET .../retrospective/action-items "Improvement Backlog" that also surfaces Phase 6's existing velocity_metrics() — reused, not reimplemented.
  • A new /sessions/[id]/retrospective page (linked from the session lobby) showing the selected closed sprint's three-column retrospective plus the session-wide Improvement Backlog.

Project Arc (Phase 10)

A read-only, session-wide longitudinal view chaining every sprint's history together — velocity, sprint outcome, live simulation risk, QA/defects, retrospective activity, and XP/achievement growth — so a team can see how it evolved across sprints instead of only ever looking at one sprint in isolation. Requires zero new database tables or columns: every number is derived, on demand, from Sprint/SprintOutcome/ SimulationEvent/RetrospectiveEntry/XpLedgerEntry/AchievementUnlock rows that already exist.

  • core/arc/service.py — the one correctness rule the whole module depends on: close_sprint() detaches any incomplete story after computing that sprint's SprintOutcome, so a CLOSED sprint's real planned/completed points must always come from the frozen SprintOutcome, never from a live capacity_metrics() call (which would under-count them). The not-yet-closed sprint is the only one that uses the live engine functions.
  • Every per-sprint entry combines, from existing engines only: outcome scores (Phase 7), live health/risk (Phase 8), defects/QA (Phase 6), a simulation event summary (Phase 8's team_simulation_analytics()), retrospective activity split into "raised here" vs. "carried in from an earlier sprint's retro" (Phase 9), the real process-risk reduction an applied action item contributes (reusing core/simulation/health.py::applied_retro_action_reduction() — made public for exactly this reuse, no second formula), and XP/achievements awarded within that sprint's real time window (the same windowing Phase 8's timeline.py already established).
  • Two small, explicit, documented formulas, both in core/arc/service.py: a _trend() helper (first-vs-last data point, polarity-aware since a rising risk is "declining" but a rising score is "improving") reused for every trend series, and a retrospective follow-through rate (APPLIED / (ADOPTED + APPLIED) — of the ideas a team committed to, how many were actually carried out). Trends are built only from CLOSED sprints — frozen, directly comparable facts — never blended with the live, still-changing active sprint.
  • One new endpoint, GET /sessions/{id}/arc — active-membership only, no SessionRole gating, matching the existing /metrics pattern (a shared team dashboard, not a private or role-restricted one). No new realtime event: the frontend simply refetches the arc on the existing event types it already knows about.
  • A new /sessions/[id]/arc page (linked from the session lobby): trend cards (velocity, outcome score, overall risk) with small recharts line charts, a retrospective follow-through summary, and a chronological sprint-by-sprint history list.

Sprint Planning Assistant (Phase 11)

Surfaces two things a Scrum Master already has, right when they need them — at the moment of creating a new sprint — instead of requiring a detour to the Metrics or Arc pages first.

  • core/arc/service.py::sprint_planning_assistant() — a suggested capacity for the new sprint, built entirely from Phase 6's existing velocity_metrics() plus the Project Arc's own _trend/ _population_stdev helpers (no new formula). The suggestion is the honest rounded average of the team's own real recent velocity; predictability (spread) is reported alongside it as context, never folded into the number, so the Scrum Master's own judgment — not an opaque formula — is what adjusts for volatility. None when there isn't yet a closed sprint to learn from.
  • One new endpoint, GET /sessions/{id}/sprints/planning-assistant — active-membership only, same shared-dashboard pattern as /metrics and /arc. The Improvement Backlog shown alongside it is not a new endpoint: the frontend reuses Phase 9's existing GET /retrospective/action-items unchanged, filtered client-side to non-terminal (OPEN/ADOPTED) items.
  • The Backlog page's "New Sprint" form gained a real capacity_points input (previously missing entirely, even though the backend already accepted it) pre-filled with the suggestion, plus the open Improvement Backlog shown right in the same modal.
  • Bug fix discovered during this phase's E2E verification: db/database.py's real engine used StaticPool — safe only for the in-memory engines every test file builds, but unsafe for this real, file-backed one: it forces every request thread to share one raw sqlite3 connection object, which reproducibly caused "database disk image is malformed" and a CPU-spinning hang under the Backlog page's normal concurrent Promise.all() page load (unchanged since Phase 2). Fixed by removing StaticPool so the real engine uses SQLAlchemy's default pooling; WAL mode already provides the concurrent-reader safety a real pool needs. Verified with an 80-request concurrent burst before and after.

Frontend Status

  • Next.js (frontend/) is the primary, actively developed UI going forward, including all future multiplayer/session/game features.
  • The legacy server-rendered UI (templates/ + static/) is frozen: it still runs (nothing has been removed), but receives no new features. It will be removed once the Next.js app has full parity with it for the standalone requirement/test-case authoring flows it currently serves.

Release Preparation

This section documents what a real (non-localhost) deployment needs, and what was fixed to make one possible. No hosting provider is assumed or configured here — see "Deployment prerequisites" below for what any provider you choose will need from you.

Required environment variables in production

Variable Purpose Required in production?
SECRET_KEY JWT signing key Yes. The app falls back to a well-known dev default and now logs a startup warning if DEBUG=false and this hasn't been set — every token is forgeable otherwise.
DEBUG false in production Controls uvicorn --reload via main.py and the SECRET_KEY check above.
CORS_ALLOWED_ORIGINS Comma-separated list of allowed frontend origin(s), e.g. https://app.example.com Yes, if the frontend is served from anywhere other than localhost:3000/3002. Unset, the API only accepts the dev localhost origins and a real frontend's requests will be rejected by CORS.
BUILDMESH_DB_URL_OVERRIDE SQLAlchemy URL for the SQLite file (e.g. to relocate it to a mounted persistent volume) Optional — defaults to data/requirements.db relative to the repo. The same variable also controls alembic upgrade head.
PORT Backend listen port when run via python main.py Optional — many hosting platforms assign this for you; defaults to 8001.
NEXT_PUBLIC_API_URL (frontend) The backend's public URL Yes — baked into the frontend at build time; must point at the real deployed backend, not 127.0.0.1.
ADMIN_USERNAME / ADMIN_EMAIL / ADMIN_PASSWORD Overrides for scripts/seed_admin.py Recommended — the script otherwise creates admin/admin123, printing a warning if you don't override the password.
LLM_ENABLED / LLM_PROVIDER / OLLAMA_* Optional local-LLM configuration (Phase 3) No — everything has a deterministic fallback; only needed if you want real LLM-generated content.

See .env.example and frontend/.env.example for the full set with inline comments.

Deployment prerequisites (provider-agnostic)

Whatever host you choose will need to:

  1. Run the backend with --ws websockets-sansio (or python main.py, which now sets this itself) — see "Real-Time Layer (Phase 5)" above for why this is not optional.
  2. Run alembic upgrade head against a persistent volume/database path before the app's first start on that database (see "Database Migrations" above).
  3. Serve the backend over HTTPS/WSS if the frontend is served over HTTPS — browsers will not open a plain ws:// connection from an https:// page.
  4. Build the frontend with NEXT_PUBLIC_API_URL already set to the backend's real public URL (it's inlined at build time, not read at request time).
  5. Persist the SQLite file (and its -wal/-shm siblings) across restarts/redeploys — either a mounted volume or, for real multi-user production use, a migration to a networked database (already flagged as future work in db/database.py's own WAL-mode comment; SQLite here is a deliberate, documented choice for this project's current scale, not an oversight).

Bugs found and fixed during release preparation

  • Production frontend build was broken. next build failed with two react/no-unescaped-entities ESLint errors (raw apostrophes in JSX text in the Backlog and Retrospective pages). Fixed; npm run build now succeeds cleanly.
  • The documented-as-required --ws websockets-sansio flag was missing from every checked-in entry point except the README's own manual instructions — main.py, frontend/package.json's dev:backend script, and frontend/scripts/dev-with-backend.mjs all started uvicorn without it, meaning anyone using python main.py or npm run dev (rather than typing the documented manual command) would hit the real, previously-documented WebSocket crash bug from the Phase 5 report. All three now pass the flag.
  • CORS origins were hardcoded to the four dev localhost URLs with no way to add a real deployment's frontend origin. Now reads CORS_ALLOWED_ORIGINS (comma-separated), falling back to the same dev defaults when unset so local development is unaffected.
  • No startup warning for an insecure default SECRET_KEY. A production deployment (DEBUG=false) that forgot to set SECRET_KEY would silently sign every JWT with a well-known, publicly-documented default. Now logs a loud warning at startup in that case (a hard failure was deliberately avoided so existing dev/test workflows, which rely on the default, are unaffected).
  • The real, file-backed database engine had no way to relocate the SQLite file without editing db/database.py — alembic's own migration runner already supported BUILDMESH_DB_URL_OVERRIDE for this; the running app now honors the same variable.
  • scripts/seed_admin.py had no way to avoid the default admin123 password other than manually changing the account afterward. Now accepts ADMIN_USERNAME/ADMIN_EMAIL/ADMIN_PASSWORD overrides and warns if the default password is still used.
  • A LICENSE file was added (MIT) — the README already declared an MIT license, but no LICENSE file existed in the repository to back it.

Deliberately not changed

  • No deployment provider, Dockerfile, or CI config was added — none was requested, and guessing a provider's exact configuration would very likely be wrong. The table above lists what any provider will need.
  • Base.metadata.create_all() still runs at app startup (see "Database Migrations" above) — removing it would change existing first-run behavior beyond what release preparation should touch; the run-migrations-first ordering is now called out explicitly instead.
  • No dependency versions were pinned beyond what requirements.txt already specified, and no new dependency was added anywhere in the stack.

License

MIT

About

BuildMesh — a multiplayer software-engineering sprint simulation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages