# Project export: Passage

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: Immigration documents, translated into your language and explained in simple terms — without your identity ever leaving your device.
- Devpost: https://devpost.com/software/passage-skdf8a
- GitHub: https://github.com/shanghanyan/Passage
- Video: https://www.youtube.com/embed/Y0ZKdVXdWmE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Ava Cheng (27 commits), Vanisha Kheterpal (3 commits), charlottewong328-dev (3 commits)

## Devpost submission (written by the team)

### Inspiration

One of our teammates is an international student, and deals with immigration paperwork often — RFEs, biometrics notices, status letters full of legal language that's genuinely stressful to parse even when English isn't the barrier. What kept bothering her wasn't just that these documents are confusing. It's that getting help with them almost always means handing over your name, your A-number, your case details, your address — to a person, a translation app, or an AI tool with a privacy policy you're just supposed to trust. For a population that's frequently already anxious about its legal status, asking them to expose more of themselves to get a letter explained felt like exactly the wrong tradeoff. We wanted to build something that didn't ask people to make that tradeoff at all. Not "we promise not to misuse your data" — an actual architecture where your identity structurally cannot reach a third party in the first place, whether that third party is an AI model, our own server logs, or our own error monitoring.

### What it does

Passage takes an official immigration document — an RFE, a biometrics notice, an EAD receipt, a Notice to Appear — and translates and explains it in plain language, in the reader's own language, across ten supported languages. Before any of that happens, everything personally identifying in the document — names, A-numbers, SSNs, dates of birth, passport numbers, addresses — is detected and replaced with a placeholder token entirely inside the browser. Nothing touches the network until you explicitly press send, and what gets sent is never the original text — only the tokenized version. You can verify this yourself, live: open your browser's network tab, and the request body to our translation endpoint contains nothing but tokens like ⟦PII:NAME:1⟧. If our automatic detection ever misses something, you're not stuck trusting it blindly — you can select the missed text yourself and redact it manually, and it's treated identically to anything caught automatically. On the way back, after Claude responds, Passage checks that every token it sent is accounted for and that no raw personal data leaked into the response. If that check fails for any reason, nothing gets displayed — not a partial result, nothing reconstructed. It fails closed. From there, you can listen to the explanation read back to you, ask follow-up questions out loud (also redacted before they reach Claude), choose how much detail you want in the explanation, and see a list of other documents commonly relevant to your specific situation, based on what kind of document you uploaded — not a generic checklist. Throughout, Passage is explicit about one boundary it never crosses: it explains what a document is asking for. It never tells you what to write back, file, or do next. That's a deliberate legal line, not a missing feature.

### How we built it

Detection runs entirely client-side: a named-entity recognition model (Xenova/bert-base-NER, via Transformers.js) running in-browser with zero network calls after the initial model download, layered with hand-written regex patterns for structured identifiers like A-numbers, SSNs, and dates. Detected spans get tokenized in the browser before anything is transmitted. The backend is a thin proxy — it holds the Anthropic API key and forwards only tokenized text to Claude (Sonnet 4.6) for translation and explanation. It never receives raw personal data either, which matters: our own server logs, our own Sentry error events, and our own observability traces can't leak what was never in that part of the system to begin with. Redis (via Upstash) stores only an ephemeral session marker, not PII, with no persistence. Two optional Redis-backed services — Agent Memory and LangCache — handle multi-turn voice conversation context and repeated-question caching, and both are scoped to store only redacted, tokenized text, never raw values. Sentry monitors our fail-closed validation path and our pre-send leakage scan, scrubbing common PII patterns from its own events as a second line of defense. Arize and Phoenix (dual-export, switchable per launch) track detection recall per document type with custom spans, so we have a real, measured accuracy number instead of a claim — including for our hardest detection case, non-Latin names. Deepgram handles voice input and read-back, transcribing questions which are then redacted client-side before ever reaching Claude, and synthesizing speech only from already-tokenized, PII-free explanation text. We built the application itself using Claude Code and Cursor throughout, and our team worked in parallel across two separate forks before merging the strongest pieces of each into one final codebase.

### Challenges we ran into

The most serious one surfaced midway through the build: we found that names were sometimes appearing in completely plain text, fully unredacted, in translated output. Tracing it down, we discovered our entire name-detection capability depended on a single point of failure — the NER model — and that model was silently failing to load on one team member's machine, due to a native binary built for a newer macOS version than the one actually running it. The app fell back to regex-only detection with no warning shown anywhere in the interface. Since our regex patterns had never covered names in the first place — only structured identifiers like A-numbers and dates — every name was leaking, invisibly, the entire time someone tested on that machine. We treated this as the architectural failure it was, not a one-off bug. The fix was a real, independent regex layer for names — label-anchored patterns that work without depending on any model loading successfully — so name detection no longer has a single point of failure. We also found and fixed a quieter bug in the same investigation: a length-based filter meant to apply only to NER-sourced spans was silently dropping legitimate regex-detected name matches too, which had been suppressing detection on certain formats (ALL CAPS names, hyphenated names) independent of the NER issue entirely. Separately, while testing translated output across languages, we caught our own system drifting past a line we'd explicitly drawn. A Spanish translation of a document with a response deadline included a line recommending the reader consult an immigration lawyer — a soft form of advice-giving, well-intentioned, but exactly the kind of thing our system prompt was supposed to prevent. We tightened the prompt to explicitly separate "stating that a deadline or consequence exists" from "recommending any course of action, including seeking legal help," and re-verified across our full test set in both languages we'd built that rule for.

### Accomplishments we're proud of

We didn't just build a privacy claim — we built one a judge, or anyone, can verify themselves in about ten seconds with their browser's devtools open, with nothing to take on faith. We're proud that when we found real failures during the build — the silent name-detection gap, the address leakage edge case, the advice-language drift in Spanish — we caught every one of them ourselves, through deliberate adversarial testing, before any of them reached a demo. We kept an honest, dated log of every failure and fix rather than quietly patching and moving on, and we think that record is some of the strongest evidence of how this project was actually built. We're also proud of the layered failure handling itself: detection that's allowed to be imperfect because we built two independent ways to catch its mistakes — a pre-send leakage scan with manual override, and post-response validation that fails closed rather than ever showing a partial or unverified result.

### What we learned

That a privacy architecture is only as strong as its quietest failure mode. The scariest bug we found in this entire build wasn't loud — it produced no error, no crash, nothing in the console. It just silently leaked, the whole time, on one specific machine. That taught us to design every detection layer assuming it will eventually fail, and to build explicit, visible checks for that failure rather than trusting any single component to always work. We also learned that a rule like "never give advice" is far easier to write into a system prompt than to actually hold under real-world phrasing and multiple languages — it took deliberately reading raw model output, not just trusting that a check passed, to catch where our own system was drifting past a boundary we thought we'd already enforced.

### What's next

Closing the one gap we're upfront about today: voice input currently reaches Deepgram as raw audio before any redaction happens on our end, so we tell users to type identifying numbers rather than say them aloud. A self-hosted speech pipeline would let voice questions get the same client-side redaction guarantee that typed and pasted text already has. We'd also like to expand full Aura-2 native voice coverage so read-back doesn't fall back to an English-accented voice for languages like Chinese, Vietnamese, or Arabic. And longer term, we want to build the citation-grounded "what does this actually mean for my specific situation" flow we deliberately scoped out of this build — it's the natural next step, but it's also exactly the kind of feature that needs to be built carefully, not quickly, given how easily it could cross from explaining a document into giving advice.

## README (from the GitHub repository)

# Passage

**UC Berkeley AI Hackathon 2026 — World Track**

Passage helps immigrants understand confusing official letters — like a notice to appear in immigration court — written in English. You paste or upload the letter and it translates and explains it in plain language in your own language (11 supported), and you can ask follow-up questions by voice.

The key idea: before anything is sent to the AI, your personal details (name, address, ID numbers) are stripped out and replaced with placeholders inside your browser, so the sensitive data never leaves your device — and you can watch in the browser's developer tools that only the placeholders get sent. It also double-checks that critical dates and deadlines survive translation correctly, since a missed deadline can sink an immigration case, and it's honest about what it can't guarantee.

**Technical (brief):** A monorepo with four packages — client (React 19 + TypeScript + Vite 6 SPA), server (Express 4 + TypeScript), shared (`@passage/shared`: sentry-scrub + explanation-text, single source to prevent drift), and a launcher (`launch.mjs` / macOS `.app`). The client runs the whole privacy pipeline in-browser: PDF/image extraction (pdf.js + Tesseract.js), PII detection (regex + Xenova/bert-base-NER via Transformers.js, recall-first at score ≥ 0.35 plus Unicode label-line names), tokenization to `⟦PII:TYPE:n⟧`, an explicit send gate, and fail-closed post-Claude validation — all orchestrated by a `usePassageFlow` phase machine (input → preview → translating → done | blocked). The server is intentionally thin (prompt + API call per feature): Claude Sonnet 4.6 for translation plus a back-translation verify pass for dates/deadlines, Deepgram for voice, Upstash + optional Redis Cloud for session/memory/cache, OpenTelemetry/OpenInference exporting to Phoenix or Arize AX, and Sentry on both ends. The thesis is that the sophistication lives in the privacy architecture and verification harness — Playwright network audits asserting no raw PII in request bodies, per-type recall metrics, fail-closed validation — not in model orchestration: **the innovation is what you don't send**.

For architecture, data flow, and file reference, see [`PROJECT_ARCHITECTURE.md`](PROJECT_ARCHITECTURE.md).

---

## First-time setup

1. **Install dependencies** (once):

```bash
npm run install:all
```

2. **Copy env files** and fill in keys:

```bash
cp server/.env.example server/.env
cp client/.env.local.example client/.env.local   # optional — browser Sentry
```

3. **Required in `server/.env`:**

| Variable | Purpose |
|---|---|
| `ANTHROPIC_API_KEY` | Claude translation + voice Q&A |
| `UPSTASH_REDIS_REST_URL` | Upstash — session markers, launcher heartbeat, rate limits |
| `UPSTASH_REDIS_REST_TOKEN` | Upstash REST credentials |
| `SENTRY_DSN` | Server error monitoring |
| `DEEPGRAM_API_KEY` | Voice transcription + TTS |
| `RECALL_ALERT_THRESHOLD` | Optional (default `0.75`) — Sentry alert when recall or NAME recall drops below |

For **Arize AX Cloud** traces with `npm run launch -- --cloud`, also set `ARIZE_SPACE_ID` and `ARIZE_API_KEY` ([app.arize.com](https://app.arize.com) → Settings).

**Optional Redis Cloud** (voice memory + FAQ cache — redacted text only):

| Variable | Purpose |
|---|---|
| `AGENT_MEMORY_URL` + `AGENT_MEMORY_STORE_ID` + `AGENT_MEMORY_API_KEY` | Multi-turn voice Q&A |
| `LANGCACHE_URL` + `LANGCACHE_CACHE_ID` + `LANGCACHE_API_KEY` | Semantic cache for repeated voice questions |

4. **macOS only — allow double-click launch** (once):

```bash
./scripts/fix-launch-app.sh
```

If macOS still warns, right-click **Launch Passage.app** → **Open** → **Open** once.

---

## Run Passage

**After setup, start Passage one of these ways** (both use the same launcher — server, client, observability picker, and auto-shutdown when you close the browser tab):

| Method | How |
|---|---|
| **macOS app** | Double-click **`Launch Passage.app`** in the repo root |
| **Terminal** | From the repo root: `npm run launch` |

Optional flags (terminal only): `--cloud` for Arize AX Cloud traces, `--local` for local Phoenix (Docker). On macOS, the app shows a dialog to pick observability instead.

```bash
npm run launch                       # default — observability picker (app) or Phoenix (terminal)
npm run launch -- --cloud            # Arize AX Cloud traces
# npm run launch -- --local          # Local Phoenix (Docker) instead
```

Re-run `./scripts/fix-launch-app.sh` only if macOS blocks the app again — not needed for every launch.

Browser opens at **http://localhost:5173**. Pick your **translation language** on the landing screen first — the whole UI (nav, redaction review, tabs, voice controls, warnings) follows that choice. **Close that tab** when you are done — the launcher stops server and client automatically.

Logs if something fails: `.passage-launch.log`

**Port conflict?** If voice or API calls fail, kill any stale server:

```bash
lsof -ti:3001 | xargs kill
npm run launch -- --cloud
```

---

## Configure secrets (reference)

The server refuses to start without working Redis and Claude credentials. See [First-time setup](#first-time-setup) for the required variables.

### Observability — pick one at launch

| Mode | Set in `.env` | Where to get keys |
|---|---|---|
| **Local Phoenix** (default) | `OBSERVABILITY_TARGET=phoenix` | No keys needed — Docker only |
| **Arize AX Cloud** | `OBSERVABILITY_TARGET=ax` | [app.arize.com](https://app.arize.com) → **Settings** → **Space ID** + **API Key** |

```bash
# Local Phoenix
OBSERVABILITY_TARGET=phoenix
PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006
PHOENIX_PROJECT_NAME=immigration-redaction-demo

# Arize AX Cloud
OBSERVABILITY_TARGET=ax
ARIZE_SPACE_ID=your-space-id
ARIZE_API_KEY=your-api-key
ARIZE_PROJECT_NAME=immigration-redaction-demo
```

Both modes use the same OpenTelemetry + OpenInference stack — Claude traces and `redaction-check` recall spans work identically; only the export destination changes.

**Launcher vs `.env`:** `Launch Passage.app` / `launch.mjs` asks which backend to use (macOS dialog) or accepts `--local` / `--cloud`. That choice is passed to the server for that session and **overrides** `OBSERVABILITY_TARGET` in `.env`. For `npm run dev` without the launcher, `.env` controls the target.

### Redis Agent (optional — voice memory + FAQ cache)

Create both services at [cloud.redis.io](https://cloud.redis.io) when you want multi-turn voice or cache hits. Only redacted/tokenized text is stored.

### Client (`client/.env.local`)

| Variable | Purpose |
|---|---|
| `VITE_SENTRY_CLIENT_DSN` | Public browser Sentry DSN (optional) |

---

## Sponsor integrations

### Anthropic (Claude)

**Plain:** Does the actual translating and plain-language explaining of the letter, and answers spoken follow-ups — but only ever sees a version with personal details swapped out for placeholders.

**Technical:** `claude-sonnet-4-6` via `@anthropic-ai/sdk`. Powers `/api/translate` (structured tool output + immigration glossary + prompt caching), a second back-translation pass for date/deadline verification, `/api/voice/question`, and `/api/related-documents`. Receives only `⟦PII:TYPE:n⟧` tokens. Backend is deliberately thin — prompt + API call per feature.

### Redis

**Plain:** Keeps the app running smoothly — remembers your session, blocks abuse, shuts down when you close the tab, and optionally remembers your voice conversation and caches repeated questions. Never stores personal info.

**Technical:** Three surfaces, all PII-free by design. **Upstash** (required): scoped session markers, per-session rate limits on translate/voice/extract, and launcher heartbeat (replaces an in-memory map). **Redis Cloud Agent Memory** (optional): tokenized multi-turn voice history with safety asserts before persist. **Redis Cloud LangCache** (optional): semantic cache for paraphrased voice FAQ, returns hit rate + similarity.

### Sentry

**Plain:** Watches for errors and raises an alarm — in

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 109 recognized source files, 531 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 146)

```
.gitignore
08-error-log.md
09-demo-script.md
client/.env.local.example
client/index.html
client/package.json
client/scripts/capture-planted-translate-payload.mjs
client/scripts/capture-screenshots.mjs
client/scripts/investigate-name-detection.ts
client/scripts/trigger-sentry-browser.mjs
client/scripts/verify-connection-lost.mjs
client/scripts/verify-demo-network.mjs
client/scripts/verify-detection.mjs
client/scripts/verify-explanation-tts.mjs
client/scripts/verify-name-detection.mjs
client/scripts/verify-redact.mjs
client/scripts/verify-regex-node.mjs
client/scripts/verify-tokenized-ui.mjs
client/scripts/verify-ui-locale-default.mjs
client/scripts/verify-upload-redaction.mjs
client/scripts/verify-validation.mjs
client/scripts/verify-voice-redaction.mjs
client/src/App.tsx
client/src/data/synthetic-docs.ts
client/src/hooks/useLauncherSession.ts
client/src/hooks/usePassageFlow.ts
client/src/i18n/locale-storage.ts
client/src/i18n/strings.ts
client/src/i18n/useUiLocale.ts
client/src/i18n/voice-tts-ui.ts
client/src/i18n/workflow-ui.ts
client/src/lib/api-fetch.ts
client/src/lib/audit-cases.ts
client/src/lib/detect.ts
client/src/lib/errors.ts
client/src/lib/explanation-text.ts
client/src/lib/extract-document.ts
client/src/lib/languages.ts
client/src/lib/leakage.ts
client/src/lib/manual-match.ts
client/src/lib/merge-spans.ts
client/src/lib/ner.ts
client/src/lib/patterns.ts
client/src/lib/prepare-voice-question.ts
client/src/lib/redact.ts
client/src/lib/redis.ts
client/src/lib/reinsert.ts
client/src/lib/score-redaction.ts
client/src/lib/sentry-scrub.ts
client/src/lib/sentry.ts
client/src/lib/token-speech.ts
client/src/lib/types.ts
client/src/lib/validate-spans.ts
client/src/lib/validate.ts
client/src/lib/voice.ts
client/src/main.tsx
client/src/styles/passage-app.css
client/src/styles/passage-v2.css
client/src/ui/AnalysisView.tsx
client/src/ui/ConnectionLostView.tsx
client/src/ui/DetectionTest.css
client/src/ui/DetectionTest.tsx
client/src/ui/EditRedactPhase.tsx
client/src/ui/ExplanationTts.tsx
client/src/ui/FileUploadZone.tsx
client/src/ui/helpers.tsx
client/src/ui/InputPhase.tsx
client/src/ui/LandingIntro.tsx
client/src/ui/LandingScroll.tsx
client/src/ui/LanguageSelect.tsx
client/src/ui/LoadingState.tsx
client/src/ui/ManualRedactPanel.tsx
client/src/ui/ManualRedactToolbar.tsx
client/src/ui/motion.tsx
client/src/ui/PassageApp.tsx
client/src/ui/PrivacyTab.tsx
client/src/ui/RelatedDocumentsTab.tsx
client/src/ui/TranslationTab.tsx
client/src/ui/VoiceTab.tsx
client/src/vite-env.d.ts
client/tsconfig.json
client/vite.config.ts
docker-compose.phoenix.yml
Launch Passage.app/Contents/_CodeSignature/CodeDirectory
Launch Passage.app/Contents/_CodeSignature/CodeRequirements
Launch Passage.app/Contents/_CodeSignature/CodeResources
Launch Passage.app/Contents/_CodeSignature/CodeSignature
Launch Passage.app/Contents/Info.plist
Launch Passage.app/Contents/MacOS/launcher
launch.mjs
package.json
passage V2 Draft.html
PROJECT_ARCHITECTURE.md
README.md
scripts/fix-launch-app.sh
server/.env.example
server/eval-dataset.jsonl
server/package.json
server/scripts/audit-sentry-payload.mjs
server/scripts/export-eval-dataset.mjs
server/scripts/score-redaction-set.ts
server/scripts/sync-synthetic-docs.mjs
server/scripts/test-document-extract.ts
server/scripts/test-explanation-text.ts
server/scripts/test-phase5.ts
server/scripts/test-phase6.ts
server/scripts/test-phases.ts
server/scripts/trigger-sentry-validation.mjs
server/src/data/synthetic-docs.json
server/src/index.ts
server/src/instrumentation.ts
server/src/lib/agent-memory.ts
server/src/lib/claude.ts
server/src/lib/deepgram-keywords.ts
server/src/lib/deepgram.ts
server/src/lib/detection-patterns.ts
server/src/lib/document-extract.ts
server/src/lib/explanation-text.ts
server/src/lib/immigration-glossary.ts
server/src/lib/lang-cache.ts
[26 more files omitted for size]
```

### Dependencies

- client/package.json: @deepgram/sdk@^5.4.0, @huggingface/transformers@^4.2.0, @passage/shared@file:../shared, @sentry/react@^10.59.0, @types/react@^19.0.2, @types/react-dom@^19.0.2, @vitejs/plugin-react@^4.3.4, pdfjs-dist@^6.0.227, playwright@^1.61.0, react@^19.0.0, react-dom@^19.0.0, tesseract.js@^7.0.0, tsx@^4.19.2, typescript@^5.7.3, vite@^6.0.7
- server/package.json: @anthropic-ai/sdk@^0.39.0, @arizeai/openinference-instrumentation-anthropic@^0.1.13, @arizeai/openinference-semantic-conventions@^2.5.0, @arizeai/phoenix-otel@^1.0.2, @opentelemetry/exporter-trace-otlp-proto@^0.219.0, @opentelemetry/resources@^2.8.0, @opentelemetry/sdk-trace-base@^2.8.0, @opentelemetry/sdk-trace-node@^2.8.0, @passage/shared@file:../shared, @sentry/node@^10.59.0, @types/cors@^2.8.17, @types/express@^5.0.0, @types/multer@^2.1.0, @types/node@^22.10.5, @upstash/redis@^1.34.3, cors@^2.8.5, dotenv@^16.4.7, express@^4.21.2, multer@^2.2.0, pdfjs-dist@^6.0.227, redis@^4.7.0, tesseract.js@^7.0.0, tsx@^4.19.2, typescript@^5.7.3

### Recent commits (newest first)

- q&a voice duplication fix
- explanation returned
- manual redaction fix + redaction count fix
- fixed voice
- cleanup
- lowered confidence threshold to catch non latin names, fixed sentry error label
- translation, sentry alert
- awaiting second round checks
- disconnect page and redact-all feature
- working product
- webpage language support
- Apply final design
- Fix name detection (regex layer + validateSpans bug), add upload/OCR, manual redaction, related documents, connection-lost view, i18n, landing scroll, redaction-bar styling
- Add files via upload
- prepare for UI edit
- functionality base done
- voice, UI, privacy build integration
- attempt merge
- voice problem, to be merged
- launcher + UIV2

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

### 08-error-log.md

```markdown
# Time | Issue | Cause | Fix | Caught by
|------|-------|-------|-----|----------|
| 2026-06-20 | Planted validation failure (demo doc) | Server simulates dropped token when `planted_validation_failure: true` | Left uncorrected on purpose — deterministic Sentry demo beat | Sentry (validation mismatch) |
| 2026-06-20 | Planted detection failure (missed address) | Address heuristic misses `Apt #4B` format without street suffix | Preview banner blocks send; `scanForLeakage` also catches Apt/Unit city-state-ZIP shapes | Sentry (pre-send leakage) |

```

### 09-demo-script.md

```markdown
# Demo script — 5 minutes

Rehearse against this throughout the build. Two failure beats are **different stories** — do not conflate them.

## Opening (15s)

One-line pitch: translates and explains immigration paperwork — your identity stays tokenized end-to-end; raw PII never leaves the browser or gets saved anywhere.

## Live demo (3m10s)

1. **(45s)** Paste a synthetic doc → **Analyze & redact** → Privacy tab shows tokens highlighted by type. Tap a token for type · confidence · source.

2. **(40s)** **Send for translation** → open devtools Network → filter `/api/` → show only `⟦PII:...⟧` in `POST /api/translate`. Pre-warm NER before opening devtools if needed.

3. **(40s)** **Fail-closed validation (Sentry)** — separate from detection gap. Run `npm run verify:sentry-browser --prefix client` once before stage, or rehearse in a spare tab: corrupted token → validation failure panel, **no** translation pane. Show Sentry event with token **keys** only.

4. **(30s)** **Date/deadline back-translate** — mention in close or if a doc with a deadline blocks when dates drift (optional live beat; Sentry event: "date/deadline drift").

5. **(40s)** Voice Q&A — emphasize **audio path ≠ text path**; show scrubbed question preview.

6. **(20s)** Translation tab: tokenized side-by-side. Raw values never persisted.

## Impact / close (1m05s)

- Privacy *architecture* vs policy — verify in devtools; volunteer [what we don't claim](../README.md#what-we-dont-claim).
- Back-translate catches **date/deadline drift**, not full translation correctness.
- Thesis: innovation is what you **don't** send.

## Optional (judge asks about detection limits)

Load **planted failure (Apt #4B)** → Analyze → yellow **Send blocked** banner while `Apt #4B` remains plain in preview. Do **not** send to Claude.

## Hard rule

No new demo beat unless it maps to a Pass/Fail row or sponsor track you're targeting.

```

### package.json

```
{
  "name": "passage",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "launch": "node launch.mjs",
    "install:all": "npm install --prefix shared && npm install --prefix server && npm install --prefix client"
  }
}

```

### shared/package.json

```
{
  "name": "@passage/shared",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "exports": {
    "./sentry-scrub": "./sentry-scrub.ts",
    "./explanation-text": "./explanation-text.ts",
    "./token-speech": "./token-speech.ts"
  }
}

```

### server/package.json

```
{
  "name": "passage-server",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js",
    "build": "tsc",
    "test:document-extract": "tsx scripts/test-document-extract.ts",
    "test:phases": "tsx scripts/test-phases.ts",
    "test:phase5": "tsx scripts/test-phase5.ts",
    "test:phase6": "tsx scripts/test-phase6.ts",
    "test:explanation-text": "tsx scripts/test-explanation-text.ts",
    "score:redaction": "tsx scripts/score-redaction-set.ts",
    "sync:synthetic-docs": "tsx scripts/sync-synthetic-docs.mjs",
    "export:eval-dataset": "tsx scripts/export-eval-dataset.mjs",
    "trigger:sentry-validation": "tsx scripts/trigger-sentry-validation.mjs",
    "audit:sentry-payload": "node scripts/audit-sentry-payload.mjs"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.39.0",
    "@arizeai/openinference-instrumentation-anthropic": "^0.1.13",
    "@arizeai/openinference-semantic-conventions": "^2.5.0",
    "@arizeai/phoenix-otel": "^1.0.2",
    "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0",
    "@opentelemetry/resources": "^2.8.0",
    "@opentelemetry/sdk-trace-base": "^2.8.0",
    "@opentelemetry/sdk-trace-node": "^2.8.0",
    "@passage/shared": "file:../shared",
    "@sentry/node": "^10.59.0",
    "@upstash/redis": "^1.34.3",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "multer": "^2.2.0",
    "pdfjs-dist": "^6.0.227",
    "redis": "^4.7.0",
    "tesseract.js": "^7.0.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^5.0.0",
    "@types/multer": "^2.1.0",
    "@types/node": "^22.10.5",
    "tsx": "^4.19.2",
    "typescript": "^5.7.3"
  }
}

```

### client/package.json

```
{
  "name": "passage-client",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "verify:regex": "tsx scripts/verify-regex-node.mjs",
    "verify:redact": "tsx scripts/verify-redact.mjs",
    "verify:validation": "tsx scripts/verify-validation.mjs",
    "verify:detection": "tsx scripts/verify-detection.mjs",
    "verify:demo-network": "tsx scripts/verify-demo-network.mjs",
    "verify:tokenized-ui": "tsx scripts/verify-tokenized-ui.mjs",
    "verify:planted-block": "tsx scripts/capture-planted-translate-payload.mjs",
    "verify:sentry-browser": "tsx scripts/trigger-sentry-browser.mjs",
    "verify:explanation-tts": "tsx scripts/verify-explanation-tts.mjs",
    "verify:voice-redaction": "tsx scripts/verify-voice-redaction.mjs",
    "verify:upload-redaction": "node scripts/verify-upload-redaction.mjs",
    "verify:ui-locale": "node scripts/verify-ui-locale-default.mjs",
    "verify:name-detection": "tsx scripts/verify-name-detection.mjs",
    "verify:all": "npm run verify:validation && npm run verify:regex && npm run verify:redact && npm run verify:voice-redaction && npm run verify:explanation-tts && npm run verify:upload-redaction && npm run verify:ui-locale && npm run verify:name-detection"
  },
  "dependencies": {
    "@deepgram/sdk": "^5.4.0",
    "@huggingface/transformers": "^4.2.0",
    "@passage/shared": "file:../shared",
    "@sentry/react": "^10.59.0",
    "pdfjs-dist": "^6.0.227",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "tesseract.js": "^7.0.0"
  },
  "devDependencies": {
    "@types/react": "^19.0.2",
    "@types/react-dom": "^19.0.2",
    "@vitejs/plugin-react": "^4.3.4",
    "playwright": "^1.61.0",
    "tsx": "^4.19.2",
    "typescript": "^5.7.3",
    "vite": "^6.0.7"
  }
}

```

### client/src/App.tsx

```typescript
import { PassageApp } from "./ui/PassageApp";
import { DetectionTest } from "./ui/DetectionTest";

export default function App() {
  if (import.meta.env.DEV && new URLSearchParams(window.location.search).has("detection-test")) {
    return <DetectionTest />;
  }
  return <PassageApp />;
}

```

### client/src/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { initSentry } from "./lib/sentry";
import App from "./App";
import "./styles/passage-v2.css";
import "./styles/passage-app.css";

initSentry();

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

```

### server/src/index.ts

```typescript
import "dotenv/config";
import "./instrumentation.js";
import cors from "cors";
import express from "express";
import multer from "multer";
import { initSentry } from "./lib/sentry.js";
import { postDeepgramToken } from "./routes/deepgram-token.js";
import { postExtractDocument } from "./routes/extract-document.js";
import { postRelatedDocuments } from "./routes/related-documents.js";
import { postRedactionSessionToken } from "./routes/redaction-session-token.js";
import { postScoreRedaction } from "./routes/score-redaction.js";
import { postTranslate } from "./routes/translate.js";
import { postVoiceTranscribe } from "./routes/voice-transcribe.js";
import { postVoiceQuestion } from "./routes/voice-question.js";
import { postVoiceSpeak } from "./routes/voice-speak.js";
import { registerLauncherRoutes } from "./routes/launcher-session.js";
import { verifyClaudeHello, verifyRedis } from "./startup.js";

initSentry();

const app = express();
const port = Number(process.env.PORT) || 3001;
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 10 * 1024 * 1024 },
});

app.use(cors());
app.post("/api/voice/transcribe", express.raw({ type: "*/*", limit: "10mb" }), (req, res) => {
  void postVoiceTranscribe(req, res);
});
app.post("/api/extract-document", upload.single("file"), (req, res) => {
  void postExtractDocument(req, res);
});
app.use(express.json());

app.get("/api/health", (_req, res) => {
  res.json({ ok: true });
});

registerLauncherRoutes(app);

app.post("/api/redaction-session-token", postRedactionSessionToken);
app.post("/api/score-redaction", postScoreRedaction);
app.post("/api/deepgram-token", (req, res) => {
  void postDeepgramToken(req, res);
});
app.post("/api/voice/question", (req, res) => {
  void postVoiceQuestion(req, res);
});
app.post("/api/voice/speak", (req, res) => {
  void postVoiceSpeak(req, res);
});
app.post("/api/translate", (req, res) => {
  void postTranslate(req, res);
});
app.post("/api/related-documents", (req, res) => {
  void postRelatedDocuments(req, res);
});

async function main() {
  try {
    await verifyRedis();
  } catch (err) {
    console.error("Redis startup check failed:", (err as Error).message);
    process.exit(1);
  }

  try {
    const hello = await verifyClaudeHello();
    console.log(`Claude hello check ok: "${hello}"`);
  } catch (err) {
    console.error("Claude startup check failed:", (err as Error).message);
    process.exit(1);
  }

  app.listen(port, () => {
    console.log(`Passage server listening on http://localhost:${port}`);
  });
}

main();

```

### server/src/lib/observability/index.ts

```typescript
import type { TracerProvider } from "@opentelemetry/api";
import { initArizeAxTracing } from "./ax.js";
import { initPhoenixTracing } from "./phoenix.js";

export type ObservabilityTarget = "phoenix" | "ax";

let enabled = false;
let activeTarget: ObservabilityTarget = "phoenix";

export function resolveObservabilityTarget(): ObservabilityTarget {
  const raw = (process.env.OBSERVABILITY_TARGET ?? "phoenix").toLowerCase();
  if (raw === "ax" || raw === "arize" || raw === "arize-ax") return "ax";
  return "phoenix";
}

export function initObservability(): TracerProvider | null {
  activeTarget = resolveObservabilityTarget();

  try {
    const provider = activeTarget === "ax" ? initArizeAxTracing() : initPhoenixTracing();
    enabled = true;
    return provider;
  } catch (err) {
    console.warn(`Observability disabled (${activeTarget}):`, (err as Error).message);
    enabled = false;
    return null;
  }
}

export function getObservabilityEnabled(): boolean {
  return enabled;
}

export function getObservabilityTarget(): ObservabilityTarget {
  return activeTarget;
}

```

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