# Project export: GOVWORLD — The Living City

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Drop any infrastructure policy into a living city of AI citizens. Watch it ripple through real lives, debate it with adversarial AI experts, and flag corruption before a single contract is signed.
- Devpost: https://devpost.com/software/govworld-the-living-city
- GitHub: https://github.com/ronak4170/GovWorld.git
- Team: 6 GitHub contributor(s) — Claude Sonnet 4.6 (5 commits), adi113003 (4 commits), ronak4170 (3 commits), Nima Sherpa (2 commits), anakamichi (1 commits), Cursor (1 commits)

## Devpost submission (written by the team)

### Inspiration

Every infrastructure decision a government makes starts somewhere as a row in a spreadsheet: a road gets widened, a budget gets approved, a contract gets signed. But the people who actually live and work on that street can become an afterthought. We kept asking one question: What if decision-makers could see how a policy affects real people before a single brick is laid? That became GOVWORLD: a human-first infrastructure simulator. Instead of treating roads, budgets, and contracts as the final output, we treat them as inputs and simulate their effects on people’s daily lives.

### What it does

GOVWORLD is a SimCity-style policy simulator that lets users place an infrastructure proposal into a living city of AI citizens, observe its consequences over time, hear competing expert perspectives, and identify accountability risks before public money moves. Our demo models San Francisco’s Van Ness Avenue Complete Streets project, a roughly $45M and 18-month infrastructure effort. A living population The city contains around 50 AI citizens. Each has a name, job, family situation, income, concerns, aspirations, and a daily route through the neighborhood. Users can click on citizens such as: Jasmine, an SFMTA bus driver Tony, a restaurant owner Earl, a retired resident with diabetes and no car They can then speak with these citizens by voice to understand how construction affects their lives. An adversarial policy council A council of ten AI expert personas, including an economist, transit engineer, climate analyst, community advocate, lawyer, and corruption watchdog, debates the proposed policy. Each expert researches relevant live information, forms a distinct perspective, and argues through synthesized speech. The goal is not to create fake agreement. The goal is to surface tradeoffs decision-makers may otherwise miss. A 12-month city simulation A generative-agent director advances the city through a 12-month timeline. It introduces realistic disruptions such as: Utility conflicts Weather delays Contractor fraud Permit challenges Legal injunctions As events unfold, citizens’ wellbeing changes from green to amber to red, showing how one infrastructure decision can ripple through jobs, mobility, health access, local business revenue, and trust in government. An accountability ledger GOVWORLD tracks contractors, budget lines, project milestones, delays, and cost overruns. It flags potential risks before they become invisible problems buried in project documentation.

### How we built it

GOVWORLD is a browser-first application built with: React 18, Vite, and TypeScript for the application Zustand for state management Leaflet and OpenStreetMap for the interactive city map react-three-fiber for the 3D debate arena A centralized llm.ts routing layer so models can be swapped by task We used different AI providers based on what each task required: Our simulation engine is inspired by Stanford’s Generative Agents research. Citizens retain memory streams, and the director retrieves relevant memories using a weighted combination of recency, importance, and relevance. This lets past events meaningfully shape future citizen reactions instead of making each simulation step feel disconnected. We also built the project with Claude Code as our pair-programmer. How we used Arize Phoenix We traced every council argument in Arize Phoenix and evaluated responses with a Claude-based judge on two criteria: Whether the expert cited named sources Whether the argument was coherent, specific, and evidence-based The evaluation exposed a real failure mode. Without an explicit instruction, our experts cited named sources 0% of the time and relied on vague claims. We added a requirement that each expert cite at least two named sources, then re-ran the evaluation. That loop of tracing, evaluating, fixing, and re-evaluating made the system measurably more accountable instead of relying on whether the outputs merely “felt good.”

### Challenges we ran into

Connecting browser-side AI calls with Python evaluation tooling Arize Phoenix evaluations are Python-based, while our application runs in the browser. We handled this through browser-side OpenTelemetry instrumentation and a CORS-enabled tracing workflow, without exposing API keys to users. Provider reliability During development, Groq was blocked on our network and Gemini’s free tier rate-limited heavily. We made our model routing provider-agnostic and moved the evaluation judge to Claude so our evaluation loop could continue. Making each simulation rerun genuinely different Our first rerolls changed the edge-case panel but did not meaningfully affect the main story. We expanded the event pool from 10 to 28 scenarios, sampled 5 to 7 seeded events per run, and injected them directly into the simulation timeline. Now each run produces a different but coherent city story. Demo reliability Hackathon judges should not have to watch loading spinners. We precomputed and cached all AI outputs for the primary demo scenario, allowing the full experience to run with zero live API calls if the network fails.

### What we learned

Building generative agents is not just about creating personalities. It is about modeling memory, reflection, cascading consequences, and compounding failures. LLM observability turns “this response feels weak” into an issue that can be measured, fixed, and re-tested. Multi-provider AI routing improves resilience, but every external dependency needs a fallback plan. The hardest part of simulating a city is not rendering the map. It is making every citizen feel like a person whose life deserves consideration.

### What's next

Scale from approximately 50 citizens to 1,000 through our prototype social-opinion swarm pipeline. Allow governments, researchers, and communities to upload a real proposed policy for any neighborhood. Connect the Arize evaluation loop to CI so every prompt change is automatically tested for evidence quality, safety, and reasoning. Expand the accountability ledger into a more robust early-warning system for delays, overruns, and procurement risks. Our north star Human-first, not infrastructure-first. The road is the input. The human life is the output.

## README (from the GitHub repository)

# GovWorld — The Living City

> An AI-populated city simulation that lets governments see the human consequences of infrastructure decisions — before a single brick is laid.

Think SimCity, but every citizen is an AI person with a name, a job, a family, and a voice. You paste a policy. The city comes alive. People react. Things go wrong. The truth comes out.

---

## What It Does

1. **Policy input** — paste any infrastructure policy (road widening, BRT lane, utility upgrade).
2. **Living map** — 50 AI citizens appear as dots moving along their daily routes on an interactive map.
3. **Council debate** — 2–7 adversarial AI experts (economist, engineer, climate analyst, community advocate, corruption watchdog, and more) stream arguments about the policy in a cinematic 3D arena, each backed by live web research.
4. **12-month simulation** — fast-forward through monthly consequences; citizen statuses update, construction overlays advance, and flags fire automatically.
5. **Voice chat** — click any citizen dot, press *Talk*, and hold a live voice conversation with them in character.
6. **Accountability ledger** — every construction task is tracked with contractor history, delay flags, and budget burn.

---

## Demo Scenario

**Van Ness Avenue Complete Streets — Phase 1**
San Francisco · Market St → Jackson St · $45M · 12 monthly ticks

### Featured Citizens

| ID | Name | Age | Role | Arc |
|----|------|-----|------|-----|
| C001 | Jasmine Chen | 34 | SFMTA Bus Driver | Route disrupted for 8 months; normalises at Month 12 |
| C002 | Tony Ricci | 57 | Restaurant Owner | Storefront access blocked; revenue drops, then recovers |
| C003 | Sofia Rodriguez | 26 | Civil Engineer | Assigned as on-site inspector from Month 2 |
| C004 | Earl Washington | 72 | Retired (diabetic, no car) | New road finally connects him to his clinic |
| C005 | Amy Park | 41 | Elementary School Teacher | Dust and noise affect her classroom for 6 months |
| C006 | Tyler Brooks | 29 | Startup Founder, cyclist | Cycling lane removed; considers relocating |

### The 90-Second Judge Demo

1. Policy is pre-loaded → press **Run Policy Council** → debate cards stream simultaneously.
2. Press **Run 12 Months** → citizen dots change colour and scaffolding appears on the map.
3. Click **Jasmine Chen** → press *Talk* → ask "How is this affecting you?" → she answers by voice, in character.
4. Scrub to Month 12 → road complete → **Earl** turns green and **Contractor B** is red-flagged in the ledger.

---

## Tech Stack

| Layer | Technology |
|-------|-----------|
| Framework | React 18 · Vite · TypeScript |
| Styling | Tailwind CSS — warm "mission control" theme (`#160c06` / `#ffb690` / `#f97316`) |
| 2D map | Leaflet + OpenStreetMap via `react-leaflet` (no API key required) |
| 3D globe | Cesium (optional, requires an Ion token) |
| 3D council arena | Three.js + `@react-three/fiber` + Drei |
| Ambient sound | Tone.js |
| State | Zustand |
| Citizens, council, simulation | **Gemini 2.5 Flash** (`@google/generative-ai`) |
| Real-time voice chat | **Groq Llama 3.3 70B** (`groq-sdk`) |
| Text-to-speech | **Deepgram Aura-2**, with Web Speech API fallback |
| Voice input | Web Speech API (`SpeechRecognition`) |
| Live council research | **Browserbase** (server-side Search + Fetch) |
| Observability & evals | **Arize Phoenix** + OpenTelemetry / OpenInference |

---

## Quick Start

### Demo mode — zero API calls

```bash
npm install
npm run demo
```

Opens at `http://localhost:5173` with all data pre-loaded from `src/data/`. No keys needed.

### Live mode

```bash
cp .env.example .env.local
# fill in your keys (see API Keys below)
npm run dev        # app only
npm run dev:full   # app + research API server (Vite + Express together)
```

`dev:full` runs Vite alongside the local Express shim (`server/dev.ts`, port 3001) that serves the Browserbase research endpoint — the same code Vercel runs as a serverless function in production (`api/research.ts`). Vite proxies `/api` → `localhost:3001`.

---

## API Keys

Copy `.env.example` to `.env.local` and fill in the keys you need. Never commit `.env.local`.

```env
# === Required for live LLM features ===
VITE_GEMINI_API_KEY=        # aistudio.google.com — free tier
VITE_GROQ_API_KEY=          # console.groq.com — free tier
VITE_DEEPGRAM_API_KEY=      # console.deepgram.com — free tier (TTS)

# === Live council research (server-side only — never bundled into the browser) ===
BROWSERBASE_API_KEY=        # browserbase.com — powers expert web research
BROWSERBASE_PROJECT_ID=

# === Optional ===
VITE_CESIUM_TOKEN=          # cesium.com/ion — only for the 3D globe view
VITE_GOOGLE_MAPS_API_KEY=   # console.cloud.google.com — not used by the Leaflet map

# === Demo flags (no keys needed) ===
VITE_DEMO_MODE=false        # true = run entirely from pre-computed JSON
VITE_SKIP_API=false         # true = force demo data even if keys are present
VITE_DEMO_NEIGHBOURHOOD=andheri  # which neighbourhood to load in demo
VITE_AUTO_DEMO=false        # true = auto-run the demo on load (unattended kiosks)
ANTHROPIC_API_KEY=
```

> **Demo mode** is active whenever `VITE_DEMO_MODE=true` or `VITE_SKIP_API=true`. Every LLM call then loads from pre-computed JSON in `src/data/` instead of hitting an API.

The `BROWSERBASE_*` keys are **not** prefixed `VITE_`, so they stay server-side and are never exposed in the client bundle.

---

## Available Scripts

| Command | What it does |
|---------|-------------|
| `npm run demo` | Start in demo mode — no API calls, fully interactive |
| `npm run dev` | Start the app in live mode with hot reload |
| `npm run dev:api` | Run only the Express research server (`server/dev.ts`) |
| `npm run dev:full` | Run Vite + the research server together (via `concurrently`) |
| `npm run live` | Live mode with Phoenix tracing enabled (`VITE_PHOENIX_TRACING=true`) |
| `npm run build` | Production build (`tsc` + Vite) |
| `npm run preview` | Preview the production build |
| `npm run precompute` | Re-generate all `src/data/` JSON via LLM (requires API keys) |
| `npm run typecheck` | TypeScript check, no emit |
| `npm run lint` | ESLint across `src/` |

---

## Live Council Research

Council experts back their arguments with real web sources via **Browserbase**:

- `api/research.ts` — Vercel serverless function: `POST /api/research` with `{ queries[], scrapeUrls?, maxFacts? }` → `{ facts[] }`.
- `api/_browserbase.ts` — the Search + Fetch implementation; holds the API key server-side.
- `server/dev.ts` — local Express shim so `npm run dev:full` works end-to-end without the Vercel CLI.

When research is unavailable, the council falls back to its pre-computed demo facts.

---

## Observability & Evals (Arize Phoenix)

The `eval/` directory wires **Arize Phoenix** into the adversarial council debate to run a full *trace → eval → close-the-loop* cycle. Every expert argument is traced, scored by an LLM-as-judge, and the failure explanations drive a measurable prompt fix.

- **Tracing** — `src/lib/tracing.ts` wraps `generateCouncilArgument` (Gemini) and `generateTurnArgument` (Groq) in OpenInference LLM spans, exported via OTLP/HTTP. The Vite dev server proxies `/v1/traces` → Phoenix at `localhost:6006`, keeping the browser same-origin (no CORS, no key in the client). Enabled only when `VITE_PHOENIX_TRACING=true` (`npm run live`); a no-op in demo mode.
- **Judge** — `eval/judge.py` runs Claude as an LLM-as-judge over council spans, scoring `evidence_citation` and `argument_quality`, logging annotations, and printing the before/after delta.
- **Seed** — `eval/seed_traces.py` emits reproducible before/after spans so the loop runs without the UI.

**Headline result:** Phoenix surfaced that, without an explicit "cite ≥2 named sources" instruction, experts cited zero sources. Adding that mandate to `EXPERT_SYSTEM_PROMPTS` in `src/lib/llm.ts` moved `evidence_citation` from 0% → 100% and `argument_quality` from 50% → 100%. See `eval/README.md` for the full run instructions.

--

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 110 recognized source files, 876 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 138)

```
.claude/agents/govworld-citizen-agent.md
.claude/agents/govworld-council-builder.md
.claude/agents/govworld-ledger-builder.md
.claude/agents/govworld-map-agent.md
.claude/agents/govworld-simulation-builder.md
.claude/agents/govworld-voice-builder.md
.env.example
.gitignore
AGENT_CITIZENS.md
AGENT_COUNCIL.md
AGENT_LEDGER.md
AGENT_MAP.md
AGENT_SIMULATION.md
AGENT_VOICE.md
api/_browserbase.ts
api/research.ts
CLAUDE.md
DESIGN.md
env.example
eval/judge.py
eval/README.md
eval/requirements.txt
eval/seed_traces.py
index.html
nvidia/DESIGN.md
package.json
postcss.config.js
README.md
scripts/.gitkeep
scripts/precompute_demo.ts
server/dev.ts
src/App.tsx
src/components/citizens/.gitkeep
src/components/citizens/CitizenCard.tsx
src/components/citizens/CitizenGenerator.tsx
src/components/citizens/CitizenList.tsx
src/components/citizens/CitizenStatus.tsx
src/components/common/CornerSquare.tsx
src/components/council/.gitkeep
src/components/council/AgentCard.tsx
src/components/council/arena/CinematicSubtitle.tsx
src/components/council/arena/CouncilArena.tsx
src/components/council/arena/CouncilChamber3D.tsx
src/components/council/arena/DebateControls.tsx
src/components/council/arena/ExpertAvatar3D.tsx
src/components/council/arena/SeverityReveal.tsx
src/components/council/CouncilDebate.tsx
src/components/council/PolicyInput.tsx
src/components/council/SeverityReport.tsx
src/components/debate/DebateArena.tsx
src/components/layout/.gitkeep
src/components/layout/FullScreenOverlay.tsx
src/components/layout/MapHud.tsx
src/components/layout/PanelManager.tsx
src/components/layout/Shell.tsx
src/components/layout/Sidebar.tsx
src/components/layout/TopBar.tsx
src/components/ledger/.gitkeep
src/components/ledger/AccountabilityLedger.tsx
src/components/ledger/ContractorFlag.tsx
src/components/ledger/TaskRow.tsx
src/components/ledger/WorkerAssignment.tsx
src/components/map/.gitkeep
src/components/map/CesiumWorld.tsx
src/components/map/CitizenDots.tsx
src/components/map/ConstructionOverlay.tsx
src/components/map/MapControls.tsx
src/components/map/NeighbourhoodSelector.tsx
src/components/notifications/DissatisfactionAlert.tsx
src/components/simulation/.gitkeep
src/components/simulation/EdgeCaseFeed.tsx
src/components/simulation/EventFeed.tsx
src/components/simulation/SimControls.tsx
src/components/simulation/SimulationEngine.tsx
src/components/simulation/TimelineBar.tsx
src/components/simulation/UpdatesFeed.tsx
src/components/swarm/KnowledgeGraphView.tsx
src/components/swarm/PredictionReportView.tsx
src/components/swarm/SwarmEngine.tsx
src/components/swarm/SwarmFeed.tsx
src/components/voice/.gitkeep
src/components/voice/CitizenChat.tsx
src/components/voice/TranscriptPanel.tsx
src/components/voice/VoiceIndicator.tsx
src/data/.gitkeep
src/data/demo_citizen_reactions.json
src/data/demo_citizens.json
src/data/demo_council_debate.json
src/data/demo_debate_sentences.json
src/data/demo_expert_research.json
src/data/demo_ledger.json
src/data/demo_policy.json
src/data/demo_simulation_ticks.json
src/data/demo_voice_responses.json
src/globals.css
src/hooks/.gitkeep
src/hooks/useCitizen.ts
src/hooks/useCouncil.ts
src/hooks/useSimulation.ts
src/hooks/useSpeech.ts
src/hooks/useWeather.ts
src/lib/.gitkeep
src/lib/cesium.ts
src/lib/constants.ts
src/lib/debateOrchestrator.ts
src/lib/deepgram.ts
src/lib/expertResearch.ts
src/lib/graphBuilder.ts
src/lib/llm.ts
src/lib/ontologyGenerator.ts
src/lib/reportAgent.ts
src/lib/seedProcessor.ts
src/lib/simulationDirector.ts
src/lib/speech.ts
src/lib/swarmSimulation.ts
src/lib/tracing.ts
src/lib/weather.ts
src/main.tsx
src/store/.gitkeep
src/store/citizenStore.ts
[18 more files omitted for size]
```

### Dependencies

- eval/requirements.txt: anthropic@==0.111.0, arize-phoenix@==17.9.0, arize-phoenix-evals@==3.1.0, google-generativeai@==0.8.6, openai@==2.43.0, opentelemetry-sdk@==1.42.1
- package.json: @arizeai/openinference-semantic-conventions@^2.5.0, @google/generative-ai@^0.21.0, @opentelemetry/api@^1.9.1, @opentelemetry/exporter-trace-otlp-http@^0.219.0, @opentelemetry/resources@^2.8.0, @opentelemetry/sdk-trace-web@^2.8.0, @opentelemetry/semantic-conventions@^1.41.1, @react-spring/three@^9.7.5, @react-three/drei@^9.114.3, @react-three/fiber@^8.17.10, @types/express@^5.0.6, @types/leaflet@^1.9.21, @types/react@^18.3.11, @types/react-dom@^18.3.1, @types/three@^0.168.0, @types/uuid@^10.0.0, @typescript-eslint/eslint-plugin@^8.8.1, @typescript-eslint/parser@^8.8.1, @vitejs/plugin-react@^4.3.2, autoprefixer@^10.4.20, cesium@^1.122.0, concurrently@^10.0.3, eslint@^9.12.0, eslint-plugin-react-hooks@^5.1.0, eslint-plugin-react-refresh@^0.4.12, express@^5.2.1, face-api.js@^0.22.2, groq-sdk@^0.7.0, leaflet@^1.9.4, postcss@^8.4.47, react@^18.3.1, react-dom@^18.3.1, react-leaflet@^4.2.1, tailwindcss@^3.4.13, three@^0.168.0, tone@^14.7.77, tsx@^4.19.1, typescript@^5.6.3, uuid@^10.0.0, vite@^5.4.8, vite-plugin-cesium@^1.2.22, vite-plugin-static-copy@^3.4.0, zustand@^5.0.1

### Recent commits (newest first)

- Update README.md
- Update README.md
- Revise README with detailed project overview and instructions
- Add Arize/Phoenix LLM observability + evals; vary live simulation
- Add Browserbase-powered live web research for council experts
- Reskin to NVIDIA design system and complete SF localization
- Reskin dashboard to mission-control UI and trim unused panels
- Update CLAUDE.md with as-built status (SF scenario, Leaflet, swarm subsystem)
- Initial commit — GOVWORLD SF mission control simulation
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### TASKS.md

```markdown
# GOVWORLD — Agent Task Board
# Claude Code Multi-Agent Coordination File
# Auto-managed by agents — do not edit manually while agents are running

## HOW THIS WORKS
Each agent claims a task by changing PENDING → IN_PROGRESS and adding their agent ID.
When complete, change to COMPLETE.
If blocked, change to BLOCKED and note the blocker.
Agents check this file before starting any work.

---

## PHASE 1 — Foundation (run these first, in parallel)

### [PENDING] TASK-001: Type System
Agent: unassigned
File: agents/AGENT_CITIZENS.md → Task 1
Creates: src/types/citizen.ts, src/types/map.ts, src/types/simulation.ts, src/types/ledger.ts, src/types/voice.ts, src/types/council.ts
Dependency: None
Note: ALL other tasks depend on types. This must complete first.
Estimated time: 30 min

### [PENDING] TASK-002: Project Scaffold
Agent: unassigned
Creates: package.json, vite.config.ts, tailwind.config.ts, tsconfig.json, .env.example, src/main.tsx, src/App.tsx, src/globals.css
Dependency: None
Note: Run npm install after this completes

---

## PHASE 2 — Data Layer (run in parallel after Phase 1)

### [PENDING] TASK-003: Generate Demo Citizens JSON
Agent: unassigned
File: agents/AGENT_CITIZENS.md → Task 2
Creates: src/data/demo_citizens.json
Dependency: TASK-001 (types)
Note: Must include exactly 6 featured citizens with specified profiles + 44 background citizens

### [PENDING] TASK-004: Generate Demo Policy + Council Debate JSON
Agent: unassigned
File: agents/AGENT_COUNCIL.md → Tasks 2, 3
Creates: src/data/demo_policy.json, src/data/demo_council_debate.json
Dependency: TASK-001 (types)
Note: Council arguments must be 180-220 words each, in persona

### [PENDING] TASK-005: Generate Demo Simulation Ticks JSON
Agent: unassigned
File: agents/AGENT_SIMULATION.md → Task 2
Creates: src/data/demo_simulation_ticks.json, src/data/demo_citizen_reactions.json
Dependency: TASK-001 (types), TASK-003 (citizen IDs must exist)
Note: All 12 ticks must have citizen updates for all 6 featured citizens

### [PENDING] TASK-006: Generate Demo Ledger JSON
Agent: unassigned
File: agents/AGENT_LEDGER.md → Task 2
Creates: src/data/demo_ledger.json
Dependency: TASK-001 (types), TASK-003 (citizen IDs for worker assignments)

### [PENDING] TASK-007: Generate Demo Voice Responses JSON
Agent: unassigned
File: agents/AGENT_VOICE.md → Task 6
Creates: src/data/demo_voice_responses.json
Dependency: TASK-003 (citizen personas)
Note: Minimum 3 Q&A pairs per featured citizen (C001-C006)

---

## PHASE 3 — Stores (run in parallel after Phase 1)

### [PENDING] TASK-008: World Store
Agent: unassigned
File: agents/AGENT_MAP.md → Task 6
Creates: src/store/worldStore.ts
Dependency: TASK-001

### [PENDING] TASK-009: Citizen Store
Agent: unassigned
File: agents/AGENT_CITIZENS.md → Task 3
Creates: src/store/citizenStore.ts
Dependency: TASK-001

### [PENDING] TASK-010: Council Store
Agent: unassigned
File: agents/AGENT_COUNCIL.md → Task 6
Creates: src/store/councilStore.ts
Dependency: TASK-001

### [PENDING]
[truncated — 4649 more characters]
```

### AGENT_MAP.md

```markdown
# AGENT_MAP — Map Agent Specification
## GOVWORLD | Map Layer Owner

---

## YOUR IDENTITY
You are the Map Agent. You own everything visual about the 3D world. You do not touch LLM calls, citizen data logic, or council debate. You render the world. Everything else feeds into you.

## FILES YOU OWN (edit ONLY these)
```
src/components/map/CesiumWorld.tsx
src/components/map/CitizenDots.tsx
src/components/map/ConstructionOverlay.tsx
src/components/map/NeighbourhoodSelector.tsx
src/components/map/MapControls.tsx
src/lib/cesium.ts
src/store/worldStore.ts
src/types/map.ts                    ← create this
```

## FILES YOU MUST NEVER EDIT
```
src/components/citizens/**
src/components/council/**
src/components/simulation/**
src/components/voice/**
src/components/ledger/**
src/store/citizenStore.ts
src/store/councilStore.ts
src/lib/llm.ts
```

## FILES YOU READ (but do not write)
```
src/store/citizenStore.ts           ← read citizen positions + statusColor
src/store/simulationStore.ts        ← read currentTick, constructionProgress
src/store/ledgerStore.ts            ← read task flags for map markers
src/data/demo_citizens.json
src/data/demo_simulation_ticks.json
```

---

## TASK 1 — Cesium World Setup (`src/lib/cesium.ts`)

### What to build
A singleton Cesium Ion viewer initialised with Google Photorealistic 3D Tiles.

### Exact implementation
```typescript
import * as Cesium from 'cesium'

const ANDHERI_EAST = {
  lat: 19.1136,
  lng: 72.8697,
  altitude: 800,   // metres above ground for initial view
}

export function initialiseCesium(containerId: string): Cesium.Viewer {
  Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_TOKEN

  const viewer = new Cesium.Viewer(containerId, {
    terrainProvider: await Cesium.createWorldTerrainAsync(),
    timeline: false,
    animation: false,
    baseLayerPicker: false,
    navigationHelpButton: false,
    homeButton: false,
    sceneModePicker: false,
    geocoder: false,
    fullscreenButton: false,
  })

  // Add Google Photorealistic 3D Tiles
  const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2275207)
  viewer.scene.primitives.add(tileset)

  // Fly to Andheri East on load
  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(
      ANDHERI_EAST.lng,
      ANDHERI_EAST.lat,
      ANDHERI_EAST.altitude
    ),
    orientation: {
      heading: Cesium.Math.toRadians(0),
      pitch: Cesium.Math.toRadians(-45),
      roll: 0,
    },
    duration: 2,
  })

  return viewer
}
```

### Environment variables needed
```
VITE_CESIUM_TOKEN=          # From cesium.com/ion — free account
VITE_GOOGLE_MAPS_API_KEY=   # From Google Cloud — Maps Tiles API
```

### Fallback (if no Cesium token)
If VITE_CESIUM_TOKEN is missing or the tile load fails, fall back to:
- Leaflet.js with OpenStreetMap tiles
- Same citizen dot overlay but on a flat 2D map
- Show a banner: "3D mode unavailable — displaying 2D map"
- The demo must still work completely on the fallback

---

## TASK 2 — Citizen Dot
[truncated — 7183 more characters]
```

### package.json

```
{
  "name": "govworld",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "dev:api": "node --env-file-if-exists=.env.example --env-file-if-exists=.env.local --import tsx server/dev.ts",
    "dev:full": "concurrently -k -n vite,api -c cyan,magenta \"vite\" \"npm:dev:api\"",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "demo": "VITE_DEMO_MODE=true VITE_SKIP_API=true vite",
    "live": "VITE_PHOENIX_TRACING=true vite",
    "precompute": "tsx scripts/precompute_demo.ts",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
  },
  "dependencies": {
    "@arizeai/openinference-semantic-conventions": "^2.5.0",
    "@google/generative-ai": "^0.21.0",
    "@opentelemetry/api": "^1.9.1",
    "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
    "@opentelemetry/resources": "^2.8.0",
    "@opentelemetry/sdk-trace-web": "^2.8.0",
    "@opentelemetry/semantic-conventions": "^1.41.1",
    "@react-spring/three": "^9.7.5",
    "@react-three/drei": "^9.114.3",
    "@react-three/fiber": "^8.17.10",
    "@types/leaflet": "^1.9.21",
    "cesium": "^1.122.0",
    "face-api.js": "^0.22.2",
    "groq-sdk": "^0.7.0",
    "leaflet": "^1.9.4",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-leaflet": "^4.2.1",
    "three": "^0.168.0",
    "tone": "^14.7.77",
    "uuid": "^10.0.0",
    "zustand": "^5.0.1"
  },
  "devDependencies": {
    "@types/express": "^5.0.6",
    "@types/react": "^18.3.11",
    "@types/react-dom": "^18.3.1",
    "@types/three": "^0.168.0",
    "@types/uuid": "^10.0.0",
    "@typescript-eslint/eslint-plugin": "^8.8.1",
    "@typescript-eslint/parser": "^8.8.1",
    "@vitejs/plugin-react": "^4.3.2",
    "autoprefixer": "^10.4.20",
    "concurrently": "^10.0.3",
    "eslint": "^9.12.0",
    "eslint-plugin-react-hooks": "^5.1.0",
    "eslint-plugin-react-refresh": "^0.4.12",
    "express": "^5.2.1",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.13",
    "tsx": "^4.19.1",
    "typescript": "^5.6.3",
    "vite": "^5.4.8",
    "vite-plugin-cesium": "^1.2.22",
    "vite-plugin-static-copy": "^3.4.0"
  }
}

```

### eval/requirements.txt

```
anthropic==0.111.0
arize-phoenix==17.9.0
arize-phoenix-evals==3.1.0
google-generativeai==0.8.6
openai==2.43.0
opentelemetry-sdk==1.42.1

```

### src/main.tsx

```typescript
import React from 'react'
import { createRoot } from 'react-dom/client'
import './globals.css'
import App from './App'
import { preloadSpeechVoices } from '@/lib/speech'

preloadSpeechVoices()

const rootElement = document.getElementById('root')
if (!rootElement) {
  throw new Error('Root element #root not found in document')
}

createRoot(rootElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

```

### src/App.tsx

```typescript
import { Suspense, lazy, useEffect, useState } from 'react'
import { useCitizenStore } from '@/store/citizenStore'
import { useCouncilStore } from '@/store/councilStore'
import { useSimulationStore } from '@/store/simulationStore'
import { useUIStore } from '@/store/uiStore'

const Shell = lazy(() =>
  import('./components/layout/Shell').catch(() => ({
    default: () => (
      <div className="flex items-center justify-center h-screen" style={{ backgroundColor: '#000000' }}>
        <div className="text-center">
          <div className="text-4xl font-bold gradient-text mb-2">GOVWORLD</div>
          <div style={{ color: '#a7a7a7' }}>Layout loading...</div>
        </div>
      </div>
    ),
  }))
)

const DEMO_POLICY_TEXT = `Van Ness Avenue Complete Streets — Phase 1 (2.3km stretch)

Budget: $45M | Duration: 18 months | Area: Van Ness Avenue, San Francisco

This project redesigns Van Ness from Market to Jackson Street with dedicated BRT lanes, protected cycle tracks, street trees, and enhanced pedestrian crossings. Phase 1 includes utility undergrounding, new storm drain installation, ADA-compliant corners, and bus shelter upgrades at 6 locations.`

function Splash({ message }: { message: string }) {
  return (
    <div className="flex items-center justify-center h-screen" style={{ backgroundColor: '#000000' }}>
      <div className="text-center">
        <div className="text-5xl font-black gradient-text mb-3 tracking-tight">GOVWORLD</div>
        <div className="text-sm mb-1" style={{ color: '#a7a7a7' }}>The Living City</div>
        <div className="text-sm mt-4 flex items-center gap-2 justify-center" style={{ color: '#757575' }}>
          <div className="w-4 h-4 border-2 border-t-transparent rounded-full animate-spin" style={{ borderColor: '#76b900', borderTopColor: 'transparent' }} />
          {message}
        </div>
      </div>
    </div>
  )
}

async function loadDemoData(
  loadCitizens: (c: any[]) => void,
  loadDebate: (members: any[], synthesis: any) => void,
  setPolicyText: (t: string) => void,
  loadTicks: (t: any[]) => void,
  setLoading: (l: boolean, msg?: string) => void
) {
  try {
    setLoading(true, 'Loading citizens...')
    const [citizens, debate, ticks] = await Promise.all([
      import('./data/demo_citizens.json').then((m) => m.default),
      import('./data/demo_council_debate.json').then((m) => m.default),
      import('./data/demo_simulation_ticks.json').then((m) => m.default),
    ])

    setLoading(true, 'Populating simulation...')

    // Load citizens
    loadCitizens(citizens as any[])

    // Load council debate
    const debateData = debate as any
    const membersArray = Object.values(debateData.members ?? debateData) as any[]
    loadDebate(membersArray, {
      overallRiskScore: debateData.synthesis?.overallRiskScore ?? 7,
      topRisks: debateData.synthesis?.topRisks ?? [],
      consensusRecommendations: debateData.synthesis?.consensusRecommendations ?? [],
    })
    setPolicyText(DEMO_POLICY_TEXT)

    // Load simulation ticks (handle both array and wrapped formats)
    const ticksArray = Array.isArray(ticks) ? ticks : (ticks as any).ticks ?? []
    loadTicks(ticksArray as any[])

    setLoading(false)
  } catch (err) {
    console.error('Demo data load failed:', err)
    setLoading(false)
  }
}

export default function App() {
  const [isReady, setIsReady] = useState(false)
  const [loadMsg, setLoadMsg] = useState('Initialising...')

  const loadCitizens = useCitizenStore((s) => s.loadCitizens)
  const loadDebate = useCouncilStore((s) => s.loadDebate)
  const setPolicyText = useCouncilStore((s) => s.setPolicyText)
  const loadTicks = useSimulationStore((s) => s.loadTicks)
  const { setLoading } = useUIStore()

  useEffect(() => {
    const setLoadingWithMsg = (loading: boolean, msg?: string) => {
      if (msg) setLoadMsg(msg)
      setLoading(loading, msg)
      if (!loading) setIsReady(true)
    }

    loadDemoData(
      loadCitizens,
      loadDebate,
      setPolicyText,
      loadTicks,
      setLoadingWithMsg
    )
  }, [])

  if (!isReady) return <Splash message={loadMsg} />

  return (
    <Suspense fallback={<Splash message="Loading interface..." />}>
      <Shell />
    </Suspense>
  )
}

```

### postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GOVWORLD | Mission Control</title>
    <meta name="description" content="Government policy simulation platform — watch AI citizens react to infrastructure decisions in real time" />
    <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='%23000'/><rect x='20' y='20' width='60' height='60' fill='%2376b900'/></svg>" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
    <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### vite.config.ts

```typescript
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { existsSync, readFileSync } from 'fs'

/** Parse a dotenv file into key/value pairs */
function parseDotEnv(filePath: string): Record<string, string> {
  if (!existsSync(filePath)) return {}
  const out: Record<string, string> = {}
  for (const line of readFileSync(filePath, 'utf-8').split('\n')) {
    const trimmed = line.trim()
    if (!trimmed || trimmed.startsWith('#')) continue
    const eq = trimmed.indexOf('=')
    if (eq === -1) continue
    out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim()
  }
  return out
}

// vite-plugin-static-copy is used for production builds only
// Dev server (npm run demo) serves Cesium assets directly from node_modules
export default defineConfig(({ mode }) => {
  // Vite only auto-loads .env / .env.local — merge .env.example so keys there are picked up
  const fromExample = parseDotEnv(path.resolve(__dirname, '.env.example'))
  const fromVite = loadEnv(mode, process.cwd(), '')
  const merged = { ...fromExample, ...fromVite }

  const envDefines: Record<string, string> = {}
  for (const [key, val] of Object.entries(merged)) {
    if (key.startsWith('VITE_')) {
      envDefines[`import.meta.env.${key}`] = JSON.stringify(val)
    }
  }

  return {
    plugins: [react()],
    define: {
      CESIUM_BASE_URL: JSON.stringify('/'),
      ...envDefines,
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
      },
    },
    optimizeDeps: {
      include: ['zustand', 'react', 'react-dom'],
    },
    server: {
      fs: { allow: ['..'] },
      proxy: {
        // Proxy Phoenix OTLP trace ingestion so the browser stays same-origin (no CORS).
        '/v1/traces': {
          target: 'http://localhost:6006',
          changeOrigin: true,
        },
        // Proxy the Browserbase research endpoint to the local dev shim
        // (server/dev.ts). In production Vercel serves api/research.ts directly.
        '/api': 'http://localhost:3001',
      },
    },
  }
})

```

### tailwind.config.ts

```typescript
import type { Config } from 'tailwindcss'

const config: Config = {
  darkMode: 'class',
  content: [
    './index.html',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        // NVIDIA design system — single green accent on black/white/gray chrome
        // (dark-chrome interpretation: control surface uses NVIDIA's black nav/footer palette)
        'primary':               '#76b900', // NVIDIA Green — the one accent
        'primary-container':     '#76b900',
        'on-primary':            '#000000',
        'on-primary-container':  '#000000',
        'primary-dark':          '#5a8d00', // pressed state
        'secondary':             '#76b900',
        'secondary-container':   '#5a8d00',
        'on-secondary':          '#000000',
        'tertiary':              '#bff230', // accent green pale — rare highlight
        'tertiary-container':    '#5a8d00',
        'on-tertiary':           '#000000',
        'error':                 '#e52020',
        'error-container':       '#650b0b',
        'on-error-container':    '#ffffff',
        'surface':               '#000000', // black canvas / chrome
        'surface-variant':       '#1a1a1a', // surface-elevated
        'surface-container':     '#0d0d0d',
        'surface-container-low': '#080808',
        'surface-container-high':'#1a1a1a',
        'surface-container-lowest': '#000000',
        'surface-container-highest': '#242424',
        'surface-dim':           '#000000',
        'surface-bright':        '#242424',
        'on-surface':            '#ffffff',
        'on-surface-variant':    '#a7a7a7', // ash — secondary text on dark
        'outline':               '#5e5e5e', // hairline-strong (dividers on dark)
        'outline-variant':       '#333333',
        'inverse-surface':       '#ffffff',
        'inverse-on-surface':    '#000000',
        'inverse-primary':       '#5a8d00',
        // NVIDIA paper-white surfaces (for any light-mode chapters)
        'canvas':                '#ffffff',
        'surface-soft':          '#f7f7f7',
        'hairline':              '#cccccc',
        'ink':                   '#000000',
        'mute':                  '#757575',
        // Override Tailwind's orange + blue ramps -> NVIDIA green (single accent).
        // Catches every stray orange-*/blue-* utility across legacy components.
        orange: {
          50:  '#f4ffd9',
          100: '#e9ffb0',
          200: '#d4f86a',
          300: '#bff230',
          400: '#9fd400',
          500: '#76b900',
          600: '#5a8d00',
          700: '#4a7300',
          800: '#3a5a00',
          900: '#2a4200',
          950: '#1a2900',
        },
        blue: {
          50:  '#f4ffd9',
          100: '#e9ffb0',
          200: '#d4f86a',
          300: '#bff230',
          400: '#9fd400',
          500: '#76b900',
          600: '#5a8d00',
          700: '#4a7300',
          800: '#3a5a00',
          900: '#2a4200',
          950: '#1a2900',
        },
        // Override Tailwind's slate ramp -> NVIDIA neutral grayscale (black -> white).
        // Kills the legacy navy chrome wherever slate-* utilities are used.
        slate: {
          50:  '#f7f7f7',
          100: '#f0f0f0',
          200: '#e0e0e0',
          300: '#cccccc',
          400: '#a7a7a7',
          500: '#757575',
          600: '#5e5e5e',
          700: '#333333',
          800: '#1a1a1a',
          900: '#0a0a0a',
          950: '#000000',
        },
        // Citizen / semantic status colours (aligned to NVIDIA semantic palette)
        citizen: {
          green: '#76b900',
          amber: '#ef9100',
          red: '#e52020',
          grey: '#757575',
        },
      },
      fontFamily: {
        sans: ['Inter', 'Arial', 'system-ui', '-apple-system', 'sans-serif'],
        mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
      },
      borderRadius: {
        // NVIDIA: aggressively angular — 2px on every interactive element, circles excepted
        none: '0px',
        xs: '1px',
        sm: '2px',
        DEFAULT: '2px',
        md: '2px',
        lg: '2px',
        xl: '2px',
        '2xl': '2px',
        '3xl': '2px',
        full: '9999px',
      },
      backgroundImage: {
        'grid-slate': 'linear-gradient(rgba(148,163,184,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(148,163,184,0.05) 1px, transparent 1px)',
      },
      backgroundSize: {
        'grid-sm': '24px 24px',
      },
      animation: {
        'pulse-slow': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
        'fade-in': 'fadeIn 0.3s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-out',
        'slide-in-right': 'slideInRight 0.3s ease-out',
        'typewriter': 'typewriter 0.05s steps(1) infinite',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(8px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
        slideInRight: {
          '0%': { transform: 'translateX(16px)', opacity: '0' },
          '100%': { transform: 'translateX(0)', opacity: '1' },
        },
      },
      boxShadow: {
        'glow-blue': '0 0 20px rgba(59,130,246,0.3)',
        'glow-green': '0 0 20px rgba(16,185,129,0.3)',
        'glow-amber': '0 0 20px rgba(245,158,11,0.3)',
        'glow-red': '0 0 20px rgba(239,68,68,0.3)',
      },
    },
  },
  plugins: [],
  // Safelist dynamic citizen status classes
  safelist: [
    'bg-emerald-500',
    'bg-amber-500',
    'bg-red-500',
    'bg-slate-500',
    'text-emerald-400',
    'text-amber-400',
    'text-red-400',
    'text-slate-400',
    'bg-emerald-900/30',
    'bg-amber-900/30',
    'bg-red-900/30',
    'bg-blue-900/30',
    'border-emerald-500',
    'border-amber-500',
    'border-red-500',
    'border-slate-500',
  ],
}

export default config

```

[100 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]