# Project export: Locked in Vision

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: Locked Vision MPI turns trust-based assembly into proof-based assembly, computer vision that waits until the live feed matches the instruction, stopping defects and downstream failures.
- Devpost: https://devpost.com/software/locked-in-vision
- GitHub: https://github.com/PranavThoppe/locked-vision-mpi
- Demo: https://docs.google.com/presentation/d/1P3W3NYKDED28E4FSCT7Ue7g8yXoeej5G/edit?usp=sharing&ouid=103939611480172920981&rtpof=true&sd=true
- Video: https://www.youtube.com/embed/rM6oZcoZ-B0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Claude Opus 4.8 (1 commits), ivancito (1 commits)

## Devpost submission (written by the team)

### Inspiration

Locked in Vision was born from the factory floor: where one missed step, one wrong part, or one unchecked detail can become rework, scrap, or risk. We wanted to build a smarter MPI system that does not just display instructions, but actually sees the work being done and verifies reality in real time.

### What it does

Locked in Vision uses computer vision to guide and verify an assembly process. For our demo, the system detects colored blocks inside an assembly area and confirms that they are placed in the correct sequence before unlocking the work order. It turns a normal checklist into a living process: the operator acts, the camera verifies, and the system only passes when the physical work is correct.

### How we built it

We built it with a Python vision bridge, a FastAPI backend, and a frontend work-instruction UI. The camera detects the blocks, the backend validates the sequence, and the UI shows the operator what step they are on. The backend is the source of truth, so the process cannot be passed by a fake button or frontend shortcut.

### Challenges we ran into

The hardest part was making the real world behave like software. Camera indexes failed, detections froze, ports conflicted, and blocks sometimes appeared visually correct but were not recognized by the system. We also had to cut the demo down to its strongest idea: block verification. Three minutes is short, so we removed extra tool and PPE steps to keep the story sharp.

### Accomplishments we're proud of

We are proud that this is not just a dashboard. It connects the digital instruction to the physical assembly space. The system proves a powerful idea: manufacturing software should not only tell operators what to do. It should help verify that the work was actually done right.

### What we learned

We learned that computer vision is powerful, but unforgiving. Lighting, camera angle, confidence thresholds, and zone calibration all matter. We also learned that a good demo is not about adding more features. It is about showing one clear truth with confidence.

### What's next

Next, we want to make Locked in Vision more reliable, flexible, and production-ready. That means better calibration, stronger detection, snapshots for audit trails, and support for real manufacturing objects like tools, parts, labels, PPE, and fixtures. The bigger vision is a smart MPI system that protects quality, supports operators, and creates trustworthy evidence automatically. Locked in Vision is the first step toward work instructions that do not just guide the process, but verify the truth.

## README (from the GitHub repository)

# 🔒 Locked Vision MPI

**A fake MES-connected physical AI workstation.** A locked camera supervises a
workstation of colored LEGO blocks and simple tools. The operator cannot advance
to the next manufacturing step unless the system *visually verifies* that the
correct part/tool moved to the correct zone in the correct order — and the work
order cannot close until every tool and part is returned home for a final 6S
reset.

> **Pitch:** This is not a camera watching a table. This is **visual proof
> connected to manufacturing execution.** The MPI only moves forward when the
> real world is correct.

Built for **UC Berkeley AI Hackathon 2026**.

---

## Problem

In real manufacturing, MPI (Manufacturing Process Instruction) compliance is
mostly trust-based: the operator clicks "Next" and the MES believes them. There
is no physical verification that the right part went to the right place in the
right order, and no guarantee the station is reset before the next job. Result:
skipped/out-of-order steps, wrong parts, un-reset stations, and audit logs that
record *clicks*, not *reality*.

## Solution

Lock the digital instruction to the physical reality:

- A **fixed overhead camera** + OpenCV detects colored LEGO blocks and tools and
  maps them to **zones**.
- A **fake MES** holds the work order and the MPI step sequence (source of truth).
- A **state machine + validation engine** compare vision evidence to the expected
  step and decide `can_advance`.
- The **dashboard** physically cannot advance unless the backend returns
  `can_advance=true`, and cannot close the work order until **final 6S** passes.
- **Every** pass and failure is written to an **audit log**.

## Architecture

```
Fake MES (truth) ─► MPI step data ─► State machine ─► Validation ─► can_advance?
                                          ▲                              │
                                          │ evidence                     ▼
                                    Vision system  ───────────────►  Audit log
                                  (mock OR OpenCV)                       │
                                                                        ▼
                                                  Frontend (obeys can_advance only)
```

Core rule: **MES is truth · vision is evidence · the state machine validates ·
the frontend never bypasses the backend.** Full detail in
[ARCHITECTURE.md](ARCHITECTURE.md).

## Repo structure

```
locked-vision-mpi/
├── backend/      FastAPI fake MES, state machine, validation, audit log
├── vision/       Camera, OpenCV color detection, zone mapping, mock vision
├── frontend/     Vite + React fake MES dashboard (gated Next button)
├── integrations/ Sponsor scaffolds (Fetch, Sentry, Redis, Deepgram, Arize)
└── docs          README · ARCHITECTURE · TEAM_ROLES · SPONSOR_INTEGRATIONS · DEMO_SCRIPT
```

## Setup

The whole system runs on **mocked vision**, so you need no camera to demo it.

### 1. Backend

```bash
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000     # http://localhost:8000/docs
```

### 2. Frontend

```bash
cd frontend
npm install
npm run dev                                    # http://localhost:5173
```

### 3. (Optional) Real vision

```bash
cd vision
pip install -r requirements.txt
python mock_vision_state.py                    # prints all mock scenarios
```

### Or with Docker

```bash
docker compose up
```

### Verify the backend

```bash
cd backend && .venv/bin/python smoke_test.py   # runs the full WO-1001 flow
```

## Demo flow (4 moments)

1. **Station readiness passes** — post `station_ready`; readiness goes green.
2. **Correct step passes** — `red → assembly`, Verify, `can_advance=true`, Next unlocks.
3. **Wrong sequence is blocked** — `blue first`, Verify → red banner, Next stays locked, failure logged.
4. **Final 6S blocks close** — `6S: tool missing` blocks; return the tool (`6S: all home`) → **Work Order Closed**.

Full run-of-show in [DEMO_SCRIPT.md](DEMO_SCRIPT.md).

## Sponsor tracks targeted

| Track | How |
|---|---|
| **Best Physical AI Hack** (primary) | Locked camera + real workstation + visual MPI gating |
| **Best Use of Fetch AI** | MES Supervisor Agent operates the gated API |
| **Best Use of Anthropic** | Built with Claude Code; Claude explains audit logs |
| **Best Use of Sentry** | Live backend/frontend/vision error monitoring |
| **Best Use of Deepgram** | Optional hands-free operator voice commands |
| **Best Use of Redis** | Optional real-time station memory + audit stream |
| **Best Use of Arize / Terac** | Optional vision evaluation + labeling loop |

Details: [SPONSOR_INTEGRATIONS.md](SPONSOR_INTEGRATIONS.md).

## Team roles

| Person | Role |
|---|---|
| 1 | Backend / MES Lead |
| 2 | Vision / Data Lead |
| 3 | Frontend / UI Lead |
| 4 | Sponsor / Agents / DevOps Lead |

Details: [TEAM_ROLES.md](TEAM_ROLES.md).

## Safety / compliance

- **No prior project code was reused.** All code in this repo is original and
  written fresh for UC Berkeley AI Hackathon 2026.
- This is a **fake/mock MES** for demonstration — not a production system and not
  to be represented as production-grade.
- No real PII or customer data; dummy work orders only.
- The audit log is demo evidence, not a certified compliance record.
- API keys live in `.env` (see `.env.example`) and are never committed.
- The camera supervises blocks and tools only — no people/biometric tracking.

## License

MIT (see hackathon submission).


## Detected evidence (automated analysis)

Indexed codebase: 52 recognized source files, 137 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (61 of 61)

```
.claude/skills/backend-mes/SKILL.md
.claude/skills/code-review/SKILL.md
.claude/skills/demo-readiness/SKILL.md
.claude/skills/frontend-ui/SKILL.md
.claude/skills/github-handoff/SKILL.md
.claude/skills/project-orchestrator/SKILL.md
.claude/skills/sponsor-integrations/SKILL.md
.claude/skills/vision-camera/SKILL.md
.env.example
.gitignore
ARCHITECTURE.md
backend/app/__init__.py
backend/app/audit_logger.py
backend/app/data/mpi_steps.json
backend/app/data/work_orders.json
backend/app/data/zones.json
backend/app/fake_mes_service.py
backend/app/main.py
backend/app/models.py
backend/app/mpi_state_machine.py
backend/app/validation_engine.py
backend/README.md
backend/requirements.txt
backend/smoke_test.py
DEMO_SCRIPT.md
docker-compose.yml
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/api.js
frontend/src/App.jsx
frontend/src/components/AuditLogPanel.jsx
frontend/src/components/DetectedObjectsPanel.jsx
frontend/src/components/ErrorBanner.jsx
frontend/src/components/Final6SCheckPanel.jsx
frontend/src/components/LiveCameraPanel.jsx
frontend/src/components/MPIStepPanel.jsx
frontend/src/components/StationReadinessPanel.jsx
frontend/src/components/WorkOrderQueue.jsx
frontend/src/main.jsx
frontend/src/styles.css
frontend/vite.config.js
integrations/arize/README.md
integrations/deepgram/README.md
integrations/fetch_agent/mes_supervisor_agent.py
integrations/fetch_agent/README.md
integrations/README.md
integrations/redis/README.md
integrations/sentry/README.md
MAIN.md
README.md
SPONSOR_INTEGRATIONS.md
TEAM_ROLES.md
vision/calibration.py
vision/camera.py
vision/color_detector.py
vision/mock_vision_state.py
vision/README.md
vision/requirements.txt
vision/snapshots/.gitkeep
vision/zone_mapper.py
```

### Dependencies

- backend/requirements.txt: fastapi@==0.111.0, pydantic@==2.7.4, uvicorn[standard]@==0.30.1
- frontend/package.json: @vitejs/plugin-react@^4.3.1, react@^18.3.1, react-dom@^18.3.1, vite@^5.3.4
- vision/requirements.txt: numpy@==1.26.4, opencv-python@==4.10.0.84, requests@==2.32.3

### Recent commits (newest first)

- Initial scaffold: Locked Vision MPI

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

### TEAM_ROLES.md

```markdown
# Team Roles — Locked Vision MPI (4 members)

One rule binds all roles: **MES is truth · vision is evidence · the state
machine validates · the frontend never bypasses the backend.**

---

## Person 1 — Backend / MES Lead
**Owns:** `backend/`

- Fake MES, work orders, MPI step JSON, zones.
- State machine, validation engine, audit log, backend API.

**Files:** `backend/app/{main,fake_mes_service,mpi_state_machine,validation_engine,audit_logger,models}.py`,
`backend/app/data/*.json(l)`.

**Expected outcome:** the backend runs the full work order flow using **mocked
vision first**, then real vision state — without ever returning
`can_advance=true` unless validation passes, and without closing a work order
until final 6S passes.

**Skill:** `/backend-mes`

---

## Person 2 — Vision / Data Lead
**Owns:** `vision/`

- Camera input, golden view, zones, OpenCV color detection.
- Zone mapping, ArUco / camera-alignment placeholder, snapshots, dataset capture.

**Files:** `vision/{camera,color_detector,zone_mapper,calibration,mock_vision_state}.py`,
`vision/snapshots/`.

**Expected outcome:** the vision service returns objects + zones for
`red_block, blue_block, yellow_block, green_block, tool_1, tool_2` (and
`finished_assembly`), in the **same shape** as the mock — so it drops into the
backend with no changes. Vision reports; it never decides flow.

**Skill:** `/vision-camera`

---

## Person 3 — Frontend / UI Lead
**Owns:** `frontend/`

- Fake MES dashboard, MPI step screen, live camera / snapshot panel.
- Detected objects panel, disabled/enabled Next Step button, error banner,
  final 6S screen, audit log UI.

**Files:** `frontend/src/App.jsx`, `frontend/src/api.js`,
`frontend/src/components/*.jsx`.

**Expected outcome:** judges understand the workflow in **10 seconds**; the UI
is readable from 6 feet; the Next button is impossible to use unless
`can_advance=true`; errors are loud.

**Skill:** `/frontend-ui`

---

## Person 4 — Sponsor / Agents / DevOps Lead
**Owns:** `integrations/`, deployment, and the docs.

- Sponsor integrations; Fetch AI Agentverse / ASI:One agent scaffold.
- Redis / Sentry / Deepgram placeholders; deployment; README; demo script;
  Devpost readiness.

**Files:** `integrations/**`, `docker-compose.yml`, `README.md`,
`SPONSOR_INTEGRATIONS.md`, `DEMO_SCRIPT.md`.

**Expected outcome:** the repo is public-ready and clearly explains which prize
tracks we target and how each integration supports the project — without any
sponsor feature becoming a dependency of the core demo.

**Skills:** `/sponsor-integrations`, `/demo-readiness`, `/github-handoff`

---

## Shared definition of done

- Backend never returns `can_advance=true` unless validation passes.
- Frontend Next button cannot be used unless `can_advance=true`.
- Wrong / out-of-order actions are blocked **and logged**.
- Work order cannot close until final 6S passes.
- App runs from a clean clone with documented commands.
- Demo runs start to finish with no manual hacks.
- Sponsor 
[truncated — 60 more characters]
```

### DEMO_SCRIPT.md

```markdown
# Demo Script — Locked Vision MPI

**Total time:** ~3 minutes. **Anchor line (say it first):**

> "This is not a camera watching a table. This is **visual proof connected to
> manufacturing execution.** The MPI only moves forward when the real world is
> correct."

---

## Setup (before judges arrive)

```bash
# Terminal 1
cd backend && source .venv/bin/activate && uvicorn app.main:app --port 8000
# Terminal 2
cd frontend && npm run dev
```

Open http://localhost:5173. Confirm "backend online" (green dot). The camera-less
demo is driven by the **Vision Simulator** buttons in the camera panel (each posts
mock vision evidence). For a physical demo, replace those with real camera pushes
(`vision/` README) — the backend behaves identically.

---

## The 4 key moments

### 1. Station readiness passes
- Click **Station ready (all home)**.
- The **Station Readiness** panel goes all-green: backend online, camera locked,
  calibration in tolerance, zones clear → **✓ STATION READY**.
- *Say:* "The station is calibrated and the camera is locked to a golden view.
  If the camera moved, we'd refuse to start."

### 2. Correct step passes
- Click **▶ Start Work Order** on WO-1001. Step 1 shows: *Move the RED block to
  the ASSEMBLY ZONE.* Next is **🔒 LOCKED**.
- Click **Red → assembly**, then **Verify Step (check vision)**.
- Banner turns green, gate reads **✓ Verified**, **NEXT STEP →** unlocks.
- Click **NEXT STEP →**. Now on step 2.
- *Say:* "The button was physically locked until the camera proved the red block
  was actually in the assembly zone."

### 3. Wrong sequence is blocked
- On step 2 (or restart at step 1 to make it crisp), click **Wrong move (blue
  first)**, then **Verify Step**.
- Loud red banner: *"Out of sequence: blue_block is in assembly_zone, but step N
  requires …"*. Next stays **🔒 LOCKED**. The failure appears in the **Audit Log**.
- *Say:* "Wrong part, wrong order — the system blocks it and logs it as evidence.
  No skipping, no faking the click."
- Recover: post the correct scenario, Verify, advance.

### 4. Final 6S blocks close until tools are returned
- Drive to the end (Blue → assembly; **Tool 1 removed** → blocked → **Tool 1
  returned** → pass; Yellow → assembly; **Finished → complete**; Next).
- The Final 6S panel appears. Click **6S: tool missing** → **Run Final 6S Check**
  → blocked: *"tool_1 must be returned to tool_1_home."*
- Click **6S: all home** → **Run Final 6S Check** → **✅ WORK ORDER CLOSED**.
- *Say:* "The work order cannot close until the station is reset — tools home,
  parts home, assembly clear. 6S enforced by vision."

---

## If something glitches (recovery)

- **Frontend error banner / backend offline:** restart Terminal 1; the dot goes
  green; state is in-memory so just re-Start the work order.
- **Wrong state shown:** click the matching Vision Simulator button to re-post
  evidence, then **Verify Step** — the backend is stateless about the *button*,
  it only trusts the last posted evidence.
- **Total f
[truncated — 345 more characters]
```

### docker-compose.yml

```yaml
# Optional convenience stack. Local dev without Docker is the primary path
# (see README). These services mount the source and install deps on start, so
# no Dockerfiles are required for the hackathon.
services:
  backend:
    image: python:3.11-slim
    working_dir: /app
    command: >
      bash -c "pip install -r requirements.txt &&
               uvicorn app.main:app --host 0.0.0.0 --port 8000"
    volumes:
      - ./backend:/app
      - ./vision:/vision      # so backend can import mock_vision_state
    environment:
      - SENTRY_DSN=${SENTRY_DSN:-}
    ports:
      - "8000:8000"

  frontend:
    image: node:20
    working_dir: /app
    command: >
      bash -c "npm install &&
               npm run dev -- --host 0.0.0.0 --port 5173"
    volumes:
      - ./frontend:/app
    environment:
      - VITE_API_URL=http://localhost:8000
    ports:
      - "5173:5173"
    depends_on:
      - backend

```

### vision/requirements.txt

```
opencv-python==4.10.0.84
numpy==1.26.4
requests==2.32.3

```

### backend/requirements.txt

```
fastapi==0.111.0
uvicorn[standard]==0.30.1
pydantic==2.7.4
# Optional sponsor integration (only used if SENTRY_DSN is set):
# sentry-sdk==2.7.1

```

### frontend/package.json

```
{
  "name": "locked-vision-mpi-frontend",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.1",
    "vite": "^5.3.4"
  }
}

```

### frontend/src/main.jsx

```javascript
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### frontend/src/App.jsx

```javascript
import { useEffect, useState } from "react";
import { api } from "./api.js";
import WorkOrderQueue from "./components/WorkOrderQueue.jsx";
import StationReadinessPanel from "./components/StationReadinessPanel.jsx";
import MPIStepPanel from "./components/MPIStepPanel.jsx";
import LiveCameraPanel from "./components/LiveCameraPanel.jsx";
import DetectedObjectsPanel from "./components/DetectedObjectsPanel.jsx";
import ErrorBanner from "./components/ErrorBanner.jsx";
import AuditLogPanel from "./components/AuditLogPanel.jsx";
import Final6SCheckPanel from "./components/Final6SCheckPanel.jsx";

export default function App() {
  const [healthOk, setHealthOk] = useState(false);
  const [workOrders, setWorkOrders] = useState([]);
  const [selectedId, setSelectedId] = useState(null);
  const [stepInfo, setStepInfo] = useState(null);
  const [validation, setValidation] = useState(null); // last validate-step result
  const [sixSResult, setSixSResult] = useState(null);
  const [objects, setObjects] = useState([]);
  const [lastScenario, setLastScenario] = useState(null);
  const [audit, setAudit] = useState([]);
  const [stationReady, setStationReady] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  async function refreshWorkOrders() {
    const list = await api.listWorkOrders();
    setWorkOrders(list);
    return list;
  }

  async function refreshStep(id) {
    const info = await api.currentStep(id);
    setStepInfo(info);
    return info;
  }

  async function refreshAudit(id) {
    const { entries } = await api.auditLog(id);
    setAudit(entries);
  }

  useEffect(() => {
    api
      .health()
      .then(() => setHealthOk(true))
      .catch(() => setHealthOk(false));
    refreshWorkOrders()
      .then((list) => {
        if (list.length) setSelectedId(list[0].work_order_id);
      })
      .catch((e) => setError(String(e)));
  }, []);

  useEffect(() => {
    if (!selectedId) return;
    refreshStep(selectedId).catch((e) => setError(String(e)));
    refreshAudit(selectedId).catch(() => {});
  }, [selectedId]);

  async function guard(fn) {
    setBusy(true);
    setError(null);
    try {
      await fn();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  }

  const onStart = (id) =>
    guard(async () => {
      await api.start(id);
      setValidation(null);
      setSixSResult(null);
      await refreshWorkOrders();
      await refreshStep(id);
      await refreshAudit(id);
    });

  const onSimulate = (scenario) =>
    guard(async () => {
      const res = await api.postVisionState(selectedId, { scenario });
      setObjects(res.objects);
      setLastScenario(scenario);
      // Station-ready evidence flips the readiness panel green.
      if (scenario === "station_ready") setStationReady(true);
    });

  const onValidate = () =>
    guard(async () => {
      const res = await api.validateStep(selectedId);
      setValidation(res);
      setObjects(res.detected_objects || objects);
      await refreshAudit(selectedId);
    });

  const onNext = () =>
    guard(async () => {
      const res = await api.advance(selectedId);
      // After advancing, the new step must be re-verified from scratch.
      setValidation(null);
      await refreshWorkOrders();
      await refreshStep(selectedId);
      await refreshAudit(selectedId);
      if (res.status !== "passed") setValidation(res);
    });

  const onSixSCheck = () =>
    guard(async () => {
      const res = await api.finalSixS(selectedId);
      setSixSResult(res);
      setObjects(res.detected_objects || objects);
      await refreshWorkOrders();
      await refreshStep(selectedId);
      await refreshAudit(selectedId);
    });

  const status = stepInfo?.status;
  const showSixS = status === "awaiting_final_6s" || status === "completed";

  return (
    <div className="app">
      <div className="topbar">
        <div>
          <h1>🔒 Locked Vision MPI</h1>
          <div className="pitch">
            Visual proof connected to manufacturing execution — the MPI only moves
            forward when the real world is correct.
          </div>
        </div>
        <div className="health">
          <span className={`dot ${healthOk ? "ok" : "bad"}`} />
          backend {healthOk ? "online" : "offline"}
        </div>
      </div>

      {error && (
        <div className="error-banner" style={{ marginBottom: 16 }}>
          <span style={{ fontSize: 22 }}>⚠️</span> {error}
        </div>
      )}

      <div className="grid">
        {/* Left column */}
        <div className="col">
          <WorkOrderQueue
            workOrders={workOrders}
            selectedId={selectedId}
            onSelect={setSelectedId}
            onStart={onStart}
          />
          <StationReadinessPanel ready={stationReady} healthOk={healthOk} />
        </div>

        {/* Center column */}
        <div className="col">
          <ErrorBanner validation={showSixS ? null : validation} />
          <MPIStepPanel
            stepInfo={stepInfo}
            validation={validation}
            onValidate={onValidate}
            onNext={onNext}
            busy={busy}
          />
          <Final6SCheckPanel
            visible={showSixS}
            result={sixSResult}
            status={status}
            onCheck={onSixSCheck}
            busy={busy}
          />
          <DetectedObjectsPanel objects={objects} />
        </div>

        {/* Right column */}
        <div className="col">
          <LiveCameraPanel
            onSimulate={onSimulate}
            lastScenario={lastScenario}
            disabled={busy || !selectedId}
          />
          <AuditLogPanel entries={audit} />
        </div>
      </div>
    </div>
  );
}

```

### backend/app/main.py

```python
"""Locked Vision MPI — backend API (FastAPI).

Architecture rules enforced here:
  * The fake MES is the source of truth.
  * The vision system provides evidence only.
  * The state machine validates the MPI sequence.
  * can_advance is set ONLY by the validation engine.
  * No work order closes until final 6S passes.
  * Every pass and every failure is logged.

Built with mocked vision first: POST a vision state (or a named mock
scenario) and the same endpoints work whether the evidence comes from
mock_vision_state.py or a real camera.
"""
import os
import sys

from fastapi import Body, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware

from .audit_logger import AuditLogger
from .fake_mes_service import FakeMESService
from .models import StepResponse, ValidationResponse, WorkOrderSummary
from .mpi_state_machine import MPIStateMachine
from .validation_engine import validate_final_6s, validate_step

# --- Optional mock vision scenarios (single source of truth in vision/) ------
# This lets the frontend post {"scenario": "step1_done"} instead of a full
# object list, which makes the camera-less demo trivial to drive.
_VISION_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), "..", "..", "vision")
)
if _VISION_DIR not in sys.path:
    sys.path.insert(0, _VISION_DIR)
try:
    from mock_vision_state import build_state as build_mock_state  # type: ignore
except Exception:  # pragma: no cover - mock module is optional at runtime
    build_mock_state = None

# --- Optional Sentry monitoring (sponsor track, isolated) --------------------
if os.getenv("SENTRY_DSN"):
    try:
        import sentry_sdk

        sentry_sdk.init(dsn=os.getenv("SENTRY_DSN"), traces_sample_rate=0.2)
    except Exception:
        pass

app = FastAPI(title="Locked Vision MPI — Fake MES", version="0.1.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # demo only; lock down for production
    allow_methods=["*"],
    allow_headers=["*"],
)

mes = FakeMESService()
sm = MPIStateMachine(mes)
audit = AuditLogger()


# --- helpers -----------------------------------------------------------------

def _require_rt(work_order_id):
    rt = sm.get(work_order_id)
    if rt is None:
        raise HTTPException(status_code=404, detail=f"Unknown work order {work_order_id}")
    return rt


def _resolve_vision(payload):
    """Turn a request body into a vision-state dict, or return None."""
    if not payload:
        return None
    if payload.get("scenario") and build_mock_state:
        return build_mock_state(payload["scenario"])
    if "objects" in payload:
        return {
            "objects": payload["objects"],
            "source": payload.get("source", "external"),
            "scenario": payload.get("scenario"),
        }
    return None


# --- endpoints ---------------------------------------------------------------

@app.get("/health")
def health():
    return {"status": "ok", "service": "locked-vision-mpi-backend"}


@app.get("/work-orders", response_model=list[WorkOrderSummary])
def list_work_orders():
    out = []
    for wo in mes.list_work_orders():
        rt = sm.get(wo["work_order_id"])
        out.append(
            WorkOrderSummary(
                work_order_id=wo["work_order_id"],
                product=wo["product"],
                mpi_id=wo["mpi_id"],
                status=rt.status,
                current_step=rt.current_step,
                total_steps=rt.total_steps,
            )
        )
    return out


@app.post("/work-orders/{work_order_id}/start", response_model=StepResponse)
def start_work_order(work_order_id: str):
    _require_rt(work_order_id)
    rt = sm.start(work_order_id)
    step = sm.current_step_def(work_order_id)
    audit.log(
        work_order_id, event="start", step=rt.current_step,
        status="in_progress", message="Work order started.",
    )
    return StepResponse(
        work_order_id=work_order_id,
        current_step=rt.current_step,
        total_steps=rt.total_steps,
        status=rt.status,
        instruction=step["instruction"] if step else "",
        step=step,
    )


@app.get("/work-orders/{work_order_id}/current-step", response_model=StepResponse)
def current_step(work_order_id: str):
    rt = _require_rt(work_order_id)
    step = sm.current_step_def(work_order_id)
    if step is None and rt.status == "awaiting_final_6s":
        instruction = "All MPI steps complete. Run the final 6S check to close the work order."
    elif step is None and rt.status == "completed":
        instruction = "Work order closed. Final 6S passed."
    elif step is None:
        instruction = "Work order not started."
    else:
        instruction = step["instruction"]
    return StepResponse(
        work_order_id=work_order_id,
        current_step=rt.current_step,
        total_steps=rt.total_steps,
        status=rt.status,
        instruction=instruction,
        step=step,
    )


@app.post("/work-orders/{work_order_id}/vision-state")
def post_vision_state(work_order_id: str, payload: dict = Body(...)):
    _require_rt(work_order_id)
    vision = _resolve_vision(payload)
    if vision is None:
        raise HTTPException(
            status_code=400,
            detail="Provide either {'objects': [...]} or {'scenario': '<name>'}.",
        )
    sm.set_vision(work_order_id, vision)
    return {
        "work_order_id": work_order_id,
        "accepted": True,
        "source": vision.get("source"),
        "scenario": vision.get("scenario"),
        "objects": vision["objects"],
    }


@app.post("/work-orders/{work_order_id}/validate-step", response_model=ValidationResponse)
def validate_current_step(work_order_id: str, payload: dict = Body(default={})):
    rt = _require_rt(work_order_id)

    # Allow vision evidence to be passed inline for convenience.
    vision = _resolve_vision(payload)
    if vision is not None:
        sm.set_vision(work_order_id, vision)

    if rt.current_step < 1:
        raise HTTP
[truncated — 4110 more characters]
```

### frontend/vite.config.js

```javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: { port: 5173 },
});

```

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