# Project export: Mechanica — A Digital Museum of Ancient Chinese Machines

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: OpenAI Build Week
- Tagline: Four lost ancient Chinese machines reborn as physically-simulated, source-cited interactive exhibits — with a GPT-5.6 docent that refuses to hallucinate.
- Devpost: https://devpost.com/software/xiaoqiang
- GitHub: https://github.com/Caleb0796/mechanica
- Demo: https://mechanica-museum.vercel.app/
- Video: https://www.youtube.com/embed/5yighBh6uZs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Caleb0796 (140 commits)

## Devpost submission (written by the team)

### Inspiration

Almost two thousand years ago, Zhang Heng built a machine that could detect an earthquake hundreds of miles away. For generations, Chinese schoolchildren have learned about it in history class—but no one alive has ever watched the original machine work. The device is long gone. All that survives is a description only a few hundred characters long. History is full of machines like this. Ancient Greece had the Antikythera mechanism, recovered from the bottom of the sea as a corroded mass of bronze. The Islamic Golden Age had al-Jazari’s water-raising machines, preserved primarily in manuscript illustrations. Song-dynasty China had Su Song’s eleven-meter astronomical clock tower, carried off after the capital fell and never made to run again. Reading about these machines is not the same as watching them work. Most museum reconstructions represent a single scholar’s interpretation, frozen in bronze and displayed motionless behind glass—which is exactly the wrong way to encounter a machine. We wanted to see the water fill the scoop, the fork yield, and the wheel advance by one cell. We wanted to see them in action.

### What it does

Mechanica is a museum of machines rebuilt from language and sketches. Each exhibit begins as a few hundred characters of classical text and becomes a complete 3D mechanism. Every part is dimensioned using versioned data with a cited source, and every movement is driven by simulated physics rather than keyframed animation. The machines actually run. Pour the water. Arm the gate. Drag the drive handle. Visitors can pull a machine apart piece by piece, zoom in on a single pivot, and select any component to see its dimensions and the source behind every value: a classical text, a measured artifact, or a clearly labeled inference where scholars had to make an educated guess. The opening collection contains four machines: Water-Powered Astronomical Clock Tower · 水运仪象台 Designed by Su Song and his engineer Han Gonglian and completed in 1092 in the Song capital. The eleven-meter tower used falling water to drive an armillary sphere, a celestial globe, and time-announcing figures. Assemble it part by part, then let the water take over. Its escapement divides continuous flow into equal beats—the ancestor of the mechanism ticking inside every mechanical watch. Water-Powered Astronomical Clock Tower · 水运仪象台 Designed by Su Song and his engineer Han Gonglian and completed in 1092 in the Song capital. The eleven-meter tower used falling water to drive an armillary sphere, a celestial globe, and time-announcing figures. Assemble it part by part, then let the water take over. Its escapement divides continuous flow into equal beats—the ancestor of the mechanism ticking inside every mechanical watch. Seismoscope · 候风地动仪 Built by Zhang Heng in 132 AD in Luoyang. A bronze vessel is ringed by eight dragons. When a distant earthquake arrives, exactly one dragon drops its ball in the direction of the quake while the other seven lock. Arguably the world’s first earthquake instrument, it appears in Mechanica as two rival reconstructions: Wang Zhenduo’s 1951 design and Feng Rui’s 2005 interpretation. One click swaps the internal mechanism. Arm the east gate, inject a pulse, and watch the east dragon fire. Seismoscope · 候风地动仪 Built by Zhang Heng in 132 AD in Luoyang. A bronze vessel is ringed by eight dragons. When a distant earthquake arrives, exactly one dragon drops its ball in the direction of the quake while the other seven lock. Arguably the world’s first earthquake instrument, it appears in Mechanica as two rival reconstructions: Wang Zhenduo’s 1951 design and Feng Rui’s 2005 interpretation. One click swaps the internal mechanism. Arm the east gate, inject a pulse, and watch the east dragon fire. Odometer Drum Carriage · 记里鼓车 A Han-dynasty invention whose complete gear counts survive in Lu Daolong’s 1027 specification. The carriage measures distance as it rolls: two decimal gear reductions translate wheel rotations into a wooden figure’s drumbeat every li and a chime every ten li. It uses the same principle as the odometer in a modern car, executed in wood a millennium earlier. Odometer Drum Carriage · 记里鼓车 A Han-dynasty invention whose complete gear counts survive in Lu Daolong’s 1027 specification. The carriage measures distance as it rolls: two decimal gear reductions translate wheel rotations into a wooden figure’s drumbeat every li and a chime every ten li. It uses the same principle as the odometer in a modern car, executed in wood a millennium earlier. Pattern Loom · 老官山提花机 Built by unnamed Han weavers around the second century BC and known through loom models excavated in 2013 from the Laoguanshan tombs in Chengdu. The loom stores a textile pattern as a mechanical program within its heddle-lift sequence, weaving the design row by row. It is a programmable machine created roughly nineteen centuries before Jacquard’s punch cards—the same type of cards later used by early computers. Pattern Loom · 老官山提花机 Built by unnamed Han weavers around the second century BC and known through loom models excavated in 2013 from the Laoguanshan tombs in Chengdu. The loom stores a textile pattern as a mechanical program within its heddle-lift sequence, weaving the design row by row. It is a programmable machine created roughly nineteen centuries before Jacquard’s punch cards—the same type of cards later used by early computers. Visitors can also explore a living exhibition stage. On the home page, the machines rotate slowly beneath a fixed museum spotlight, entering the light one at a time.

### How we built it

Every line of code was written by OpenAI Codex. We planned the architecture, reviewed the work, and made the decisions; Codex typed the code. One long-lived session orchestrated the full build, and that session’s ID is included with this submission. On the final day, three Codex sessions ran in parallel across separate git worktrees. Their work followed a 28-task QA plan with strict file ownership, isolated checkpoints, and controlled merges. The project’s house rule is simple: Data is law. Machine dimensions live in versioned JSON files, with a source attached to every value. Application code is never allowed to invent a number. The final build passed: 267 unit tests 67 Playwright browser scenarios A custom geometry-validation suite Challenges Rebuilding machines nobody alive has seen No complete drawings survive. The available dimensions come from a few hundred characters of Song-dynasty text, Han tomb artifacts, and modern papers whose authors often disagree. To represent that uncertainty honestly, every value in the data model includes both its source and its epistemic status: Text-attested Artifact-measured Scholar-inferred When scholars disagree, the museum presents multiple reconstructions rather than pretending that one interpretation is certain. Making the simulation honest Su Song’s escapement is a self-regulating feedback loop: The scoop fills. The fork yields. The gate opens. The wheel advances exactly one cell. The mechanism locks again. That behavior must emerge from the mechanism’s simulated state. It cannot be a keyframed animation; otherwise, a “working reconstruction” would be theater. The seismoscope follows the same standard. Its first-event interlock must ensure that one dragon fires, the other seven lock, and only the armed direction can trigger. Proving the geometry The clock tower alone contains 87 interlocking parts. A custom validator sweeps every mechanism through its complete motion cycle at half-degree resolution. Each run performs roughly 4,000 checks covering: Part collisions Gear meshing Motion envelopes Mechanical clearances Codex also wrote poison tests that deliberately corrupt the data—for example, by inserting an incorrect gear ratio or deleting a required source—to prove that the validator detects genuine failures. In that suite, a green result would be the bug. All four poisons were caught. Keeping the AI honest The docent may never state a number that the museum’s corpus cannot support. That constraint runs through the entire stack: Per-value sources in the JSON A prompt contract that forbids unsupported dimensional arithmetic Citation chips embedded in streamed responses A refusal path tested like any other product feature Most chatbot demos showcase confidence. Ours showcases restraint. Solving the ordinary kind of hard Not every challenge was historical or theoretical. An idle auto-pause silently stopped demonstrations after three seconds. We discovered it by clicking through the museum like a bored visitor rather than following the intended path. Later, at 9 p.m. on the night before submission, the serverless runtime began returning 500 errors because of bare JSON imports. Both issues became part of the final QA process.

### What's next

The museum was designed to grow. Its provenance schema is deliberately culture-agnostic. A dimension sourced from Vitruvius or al-Jazari’s 1206 manuscript receives the same three-way classification as a dimension taken from a Song-dynasty treatise: Text-attested Artifact-measured Scholar-inferred The validator does not care which language the historical argument was written in. The roadmap is therefore simple: More wings. Same law. The next machine: the South-Pointing Chariot · 指南车 The South-Pointing Chariot carries a wooden figure that continues pointing south regardless of how the carriage turns. Its gear counts survive in Song court records, much like those of the odometer. Whether those gears constitute a true differential remains the subject of scholarly debate—exactly the kind of disagreement this museum was built to stage. The first wings beyond China Future exhibits could include: The Antikythera mechanism, whose rival reconstructions have become a scholarly genre of their own Al-Jazari’s water-raising machines Hero of Alexandria’s automata Each exhibit would follow the same architecture: the same JSON schema, the same validator, and the same rule that code may never invent a number. A docent that can leave the building A future citation-allowlisted web mode would allow the docent to search beyond the museum’s internal corpus—but only within pre-vetted scholarly sources. Every claim would still require a pinned citation. Classroom mode Guided tours could help teachers use the machines as lessons: Run the escapement as a physics demonstration. Explore the gears as a mechanical-engineering exercise. Use provenance labels to teach a more difficult lesson: the difference between what we know and what we infer.

## README (from the GitHub repository)

# MECHANICA

MECHANICA is a bilingual digital museum where four lost ancient Chinese
machines run again as physically simulated, source-cited 3D reconstructions.

## Live demo

[Open the museum](https://mechanica-museum.vercel.app).

No login is required, and the museum works in any modern browser.

## The four machines

- **Zhang Heng's Seismoscope (候风地动仪)** — Eastern Han, 132 CE,
  Luoyang.
- **Water-Powered Astronomical Clock Tower (水运仪象台)** — Northern Song;
  a wooden model was made in 1088 and the tower was completed at Bianjing in 1092.
- **Odometer Drum Carriage (记里鼓车（大章车）)** — Han origin; Lu Daolong's
  complete tooth counts survive from 1027.
- **Laoguanshan Han Pattern Loom (老官山一勾多综提花织机)** — Western Han,
  about 157–88 BCE; excavated in 2013 from Laoguanshan tomb M2 in Chengdu.

These dates and names come directly from the machine manifests in
[`src/data/machines`](./src/data/machines).

## What makes it different

- **Every dimension is traceable.** Measurements and modeled parts carry
  provenance as a historical text, a surviving artifact, or a stated
  inference. The interface exposes that distinction instead of flattening it
  into false certainty.
- **Rival reconstructions remain visible.** Scholarly alternatives can be
  compared side by side and swapped with one click. The seismoscope, for
  example, preserves both Wang Zhenduo's and Feng Rui's proposed internal
  mechanisms.
- **The machines work.** These are driveable simulations, not turntable-only
  models. A visitor can arm a bearing, inject a quake, and watch the causal
  chain travel through the mechanism.
- **Chinese and English are first-class.** Navigation, machine records,
  explanations, controls, captions, and the docent are bilingual throughout.

## Built with Codex + GPT-5.6

I planned; Codex typed. Every line of code in this repo was written by OpenAI
Codex sessions.

I ran three Codex sessions in parallel. Each session owned its own files, and
their work merged only through checkpoint gates. That separation mattered:
geometry, interaction, and presentation work could proceed concurrently
without silently rewriting one another's evidence.

[`AGENTS.md`](./AGENTS.md) declares the working rules. In particular, data is
law: `src/data/**`, machine facts, and the validator are off-limits to visual
sculpting tasks. A more attractive reconstruction cannot overrule a cited
measurement or quietly erase an inference label.

The in-app docent runs on GPT-5.6 through the OpenAI Responses API in
[`api/docent.ts`](./api/docent.ts). The API key stays on the server. Answers
stream to the browser over server-sent events, and source markers become
citation chips in the interface.

The docent receives only the current museum record, selected part and
reconstruction, plus the four-machine index. Its instructions require a
source marker after factual claims and require it to refuse questions outside
the museum data instead of filling gaps with invented facts.

This division is deliberate: Codex built and checked the museum, while the
runtime model is constrained to explain the evidence already committed to the
repository.

## Verification

The current local test run passes **267 unit tests across 30 test files**:

```bash
pnpm test
```

The repository also has three higher-level gates:

- `pnpm e2e` builds the application and runs the Playwright browser scenario
  gate.
- `pnpm validate` runs [`scripts/validate.mts`](./scripts/validate.mts), which
  checks data and source snapshots, resolves each reconstruction, and sweeps
  geometry, motion ranges, collisions, ratios, and provenance over thousands
  of sampled states.
- `pnpm poison` runs
  [`scripts/poison-test.mts`](./scripts/poison-test.mts). It deliberately
  corrupts a gear ratio, a joint range, a transient collision, and dimension
  provenance to prove that the validator catches every planted needle.

Validation is not a claim that a disputed historical reconstruction is the
only correct answer. It checks that each declared reconstruction is internally
consistent, mechanically inspectable, and honest about the evidence behind
it.

## Quickstart

Requirements: a current Node.js installation and `pnpm`.

Install dependencies and start the local Vite server:

```bash
pnpm install && pnpm dev
```

Open the local URL printed by Vite. The museum itself does not require an
OpenAI API key.

Optional verification commands:

```bash
pnpm test
pnpm e2e
pnpm validate
```

## Optional docent setup

The docent is an optional server-side feature. Set the following environment
variable in the deployment environment:

```bash
OPENAI_API_KEY=your_server_side_key
```

`OPENAI_MODEL` is optional; the current default is `gpt-5.6`.

The docent also requires a shared daily-limit store. For an environment
without KV, explicitly opt into the approximate in-memory limit:

```bash
DOCENT_ACCEPT_APPROX_LIMITS=1
```

Without `OPENAI_API_KEY`, the docent hides itself and the rest of the museum
continues to work normally.

Do not expose the API key through client-side Vite variables or commit it to
the repository.

## OpenAI Build Week 2026

Built for **OpenAI Build Week 2026 — Education**.


## Detected evidence (automated analysis)

Indexed codebase: 139 recognized source files, 2069 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 225)

```
.codex/ultracode/runs/20260717-mechanica/ledger.md
.codex/ultracode/runs/20260719-0026/ledger.md
.codex/ultracode/runs/20260719-0026/wave-0.start
.github/workflows/ci.yml
.gitignore
AGENTS.md
api/.gitkeep
api/docent.ts
artifacts/audits/.gitkeep
artifacts/audits/astroclock.md
artifacts/audits/loom.md
artifacts/audits/odometer.md
artifacts/audits/seismoscope.md
artifacts/extractions/.gitkeep
artifacts/extractions/astroclock.json
artifacts/extractions/loom.json
artifacts/extractions/odometer.json
artifacts/extractions/seismoscope.json
artifacts/source-snapshots/.gitkeep
artifacts/source-snapshots/astroclock/xyxfy-action.json
artifacts/source-snapshots/astroclock/xyxfy-baoshi.json
artifacts/source-snapshots/astroclock/xyxfy-shulun.json
artifacts/source-snapshots/astroclock/xyxfy-taiti.json
artifacts/source-snapshots/astroclock/xyxfy-tianheng.json
artifacts/source-snapshots/astroclock/xyxfy-water.json
artifacts/source-snapshots/bellows/hhs-dushi.json
artifacts/source-snapshots/bellows/nongshu-hanji.json
artifacts/source-snapshots/bellows/nongshu-shuipai.json
artifacts/source-snapshots/chainpump/hhs-bilan.json
artifacts/source-snapshots/chainpump/nongshu-fanche.json
artifacts/source-snapshots/chainpump/sgz-majun-fanche.json
artifacts/source-snapshots/chariot/sanguozhi-majun.json
artifacts/source-snapshots/chariot/songshi-turn.json
artifacts/source-snapshots/chariot/songshi-wuderen.json
artifacts/source-snapshots/chariot/songshi-yansu.json
artifacts/source-snapshots/gimbal/bttc-hejiacun-bowl.json
artifacts/source-snapshots/gimbal/chnmuseum-famen.json
artifacts/source-snapshots/gimbal/sxhm-hejiacun.json
artifacts/source-snapshots/gimbal/xijingzaji-dinghuan.json
artifacts/source-snapshots/loom/kaogu-laoguanshan.json
artifacts/source-snapshots/loom/sgz-majun-loom.json
artifacts/source-snapshots/loom/xijingzaji-loom.json
artifacts/source-snapshots/odometer/songshi-ludaolong.json
artifacts/source-snapshots/odometer/songshi-waiguan.json
artifacts/source-snapshots/seismoscope/houfeng-196.json
artifacts/source-snapshots/typecase/mengxi-bisheng.json
artifacts/source-snapshots/typecase/nongshu-shiyin.json
artifacts/source-snapshots/typecase/nongshu-zaolun.json
artifacts/source-snapshots/wooden-ox/sgz-benzhuan.json
artifacts/source-snapshots/wooden-ox/sgz-liuma.json
artifacts/source-snapshots/wooden-ox/sgz-muniu.json
artifacts/source-snapshots/wooden-ox/shiwujiyuan.json
docs/DEPLOY.md
docs/OPEN_QUESTIONS.md
docs/SUBMISSION_NOTES.md
e2e/aids.spec.ts
e2e/astroclock-ux.spec.ts
e2e/composite.spec.ts
e2e/machines-ux.spec.ts
e2e/shoot.spec.ts
e2e/smoke.spec.ts
e2e/story.spec.ts
index.html
LICENSE
machines.csv
MECHANICA_ASTROCLOCK_QA_PLAN_EN.md
MECHANICA_FIDELITY_PLAN_EN.md
MECHANICA_PLAN_EN.md
netlify.toml
package.json
playwright.config.ts
pnpm-workspace.yaml
public/assets/IMAGE_CREDITS.json
public/assets/museum/.gitkeep
README.md
reports/astroclock.validation.json
reports/image-fetch-failures.json
reports/loom.validation.json
reports/odometer.validation.json
reports/seismoscope.validation.json
reports/summary.md
scripts/audit-llm.mjs
scripts/check-artifacts.mjs
scripts/check-i18n.mts
scripts/extract-parts.mjs
scripts/fetch-images.mjs
scripts/poison-test.mts
scripts/render-renders.mjs
scripts/snapshot-sources.mts
scripts/validate.mts
src/core/gears.ts
src/core/geometryCache.ts
src/core/geometryUvs.ts
src/core/geometryWarmup.ts
src/core/materialCache.ts
src/core/materials.ts
src/core/primitives.ts
src/core/textures.ts
src/core/units.ts
src/data/machines/.gitkeep
src/data/machines/astroclock.json
src/data/machines/loom.json
src/data/machines/odometer.json
src/data/machines/seismoscope.json
src/data/reconstructionRenders.ts
src/data/schema.ts
src/machines/.gitkeep
src/machines/astroclock/build.ts
src/machines/astroclock/parts.json
src/machines/astroclock/scene.ts
src/machines/astroclock/schemes/combridge-hinged.json
src/machines/astroclock/schemes/fixed-scoop.json
src/machines/astroclock/story.ts
src/machines/loom/build.ts
src/machines/loom/parts.json
src/machines/loom/scene.ts
src/machines/loom/schemes/linkage.json
src/machines/loom/schemes/sliding-frame.json
src/machines/odometer/build.ts
src/machines/odometer/parts.json
[105 more files omitted for size]
```

### Dependencies

- package.json: @playwright/test@^1.55.0, @react-three/drei@^10.7.4, @react-three/fiber@^9.3.0, @types/react@^19.1.10, @types/react-dom@^19.1.7, @types/three@^0.179.0, @vitejs/plugin-react@^4.7.0, eslint@^9.34.0, i18next@^25.3.4, playwright@^1.55.0, prettier@^3.6.2, react@^19.1.1, react-dom@^19.1.1, react-i18next@^15.7.3, three@^0.179.1, three-mesh-bvh@^0.9.1, tsx@^4.20.5, typescript@5.7.3, vite@^6.4.1, vitest@^3.2.4, zustand@^5.0.8

### Recent commits (newest first)

- docs: update unit test count
- ci: shard full validation by machine
- Merge remote-tracking branch 'origin/fix/weld-clearance'
- fix(seismoscope): clear dragon mount welds
- fix(viewer): tier aid cutaway opacity
- test(odometer): type frozen striker envelopes
- Merge remote-tracking branch 'origin/feature/odometer-arms'
- Merge remote-tracking branch 'origin/feature/texture-pass'
- ci: run browser gate nightly
- test(e2e): align merged playback contracts
- fix(odometer): articulate striker arms
- Merge remote-tracking branch 'origin/fix/visual-pack'
- Merge remote-tracking branch 'origin/feature/seismoscope-animals'
- feat(rendering): enrich procedural surfaces
- test(ci): extend passing validator ratio timeout
- docs: add repository README
- test(ci): extend timeouts for slow clearance and validator tests
- fix(viewer): make demo playback interruptible
- fix(seismoscope): contain dragon alarm balls
- fix(home): fit turntable controls above fold

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

### AGENTS.md

```markdown
# Mechanica — Agent Conventions
- Build/test commands (root/human only): pnpm install · pnpm dev · pnpm test · pnpm validate · pnpm e2e · pnpm build
- Data is law: numbers in src/data and src/machines follow MECHANICA_PLAN_EN.md §7 as the single source of truth; no "helpful corrections".
- Three-tier provenance: every part & dimension carries provenance {kind: wenxian|wenwu|tuice, ref}; absence = validation error.
- Units: meters internally; ancient-unit conversion only via src/core/units.ts.
- Ownership: one owner per src/machines/<slug> directory; cross-directory edits must be declared in the task notes first.
- Commits: conventional commits; one commit per task; node_modules and raw large images stay out;
  optimized site assets produced by fetch-images/render pipeline (≤300KB each, 25MB total budget) MUST be committed —
  a clean clone must build and deploy.
- Red validation = stop merging; fix first. Poison tests (scripts/poison-test.mts) are EXPECTED to go red — green is the bug.
- Raw run logs/transcripts never enter the repo; sanitize committed artifacts (paths / session IDs / org IDs / internal endpoints).

## Recurring failure patterns
- A `sourceRef` may represent a composite source (`source-a + source-b`); resolve and validate every trimmed component, never the concatenated string as one source id.
- Changes to `src/machines/<slug>/build.ts`, `parts.json`, or schemes must be checked against the resolved base spec and every scheme patch; do not validate only the raw JSON.
- Custom/composite geometry must preserve valid bounds, normals, and material metadata after merge/transform operations; dispose of temporary geometries after a successful merge.
- Viewer or geometry changes need both deterministic validation and the visual gate before merge; retain generated optimized render assets, but never add raw captures or run logs.

```

### reports/summary.md

```markdown
# Mechanica validation summary

| Machine | Pass | Fail | Warn | Resolution |
| --- | ---: | ---: | ---: | ---: |
| astroclock | 1726 | 3 | 0 | 0.500000° |
| seismoscope | 1116 | 0 | 0 | 0.500000° |
| odometer | 518 | 2 | 0 | 1.890000° |
| loom | 684 | 3 | 0 | 0.499990° |

```

### package.json

```
{
  "name": "mechanica",
  "private": true,
  "version": "0.0.0",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "test": "vitest run",
    "validate": "tsx scripts/validate.mts",
    "poison": "tsx scripts/poison-test.mts",
    "e2e": "VITE_E2E=1 vite build && playwright test",
    "i18n:check": "tsx scripts/check-i18n.mts",
    "snapshot-sources": "tsx scripts/snapshot-sources.mts",
    "fetch-images": "node scripts/fetch-images.mjs",
    "render-assets": "node scripts/render-renders.mjs",
    "extract": "node scripts/extract-parts.mjs",
    "audit": "node scripts/audit-llm.mjs",
    "artifacts:check": "node scripts/check-artifacts.mjs"
  },
  "dependencies": {
    "@react-three/drei": "^10.7.4",
    "@react-three/fiber": "^9.3.0",
    "i18next": "^25.3.4",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "react-i18next": "^15.7.3",
    "three": "^0.179.1",
    "three-mesh-bvh": "^0.9.1",
    "zustand": "^5.0.8"
  },
  "devDependencies": {
    "@playwright/test": "^1.55.0",
    "@types/react": "^19.1.10",
    "@types/react-dom": "^19.1.7",
    "@types/three": "^0.179.0",
    "@vitejs/plugin-react": "^4.7.0",
    "eslint": "^9.34.0",
    "playwright": "^1.55.0",
    "prettier": "^3.6.2",
    "tsx": "^4.20.5",
    "typescript": "5.7.3",
    "vite": "^6.4.1",
    "vitest": "^3.2.4"
  }
}

```

### src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

import App from './ui/App'

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

```

### src/ui/App.tsx

```typescript
import { Component, type ReactNode, useEffect } from 'react'
import { useTranslation } from 'react-i18next'

import './i18n'
import './styles.css'
import RouterView from './routes'
import { type UiLanguage, useUiStore } from './store'

interface LazyRouteErrorBoundaryProps {
  children: ReactNode
  message: string
  reloadLabel: string
}

interface LazyRouteErrorBoundaryState {
  failed: boolean
}

export class LazyRouteErrorBoundary extends Component<
  LazyRouteErrorBoundaryProps,
  LazyRouteErrorBoundaryState
> {
  state = { failed: false }

  static getDerivedStateFromError() {
    return { failed: true }
  }

  render() {
    if (this.state.failed) {
      return (
        <main className="error-page" role="alert">
          <h1>{this.props.message}</h1>
          <button
            className="gold-button"
            onClick={() => window.location.reload()}
            type="button"
          >
            {this.props.reloadLabel}
          </button>
        </main>
      )
    }

    return this.props.children
  }
}

function LanguageSwitch() {
  const { i18n, t } = useTranslation()
  const language = useUiStore((state) => state.language)
  const setLanguage = useUiStore((state) => state.setLanguage)

  useEffect(() => {
    document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en'
  }, [language])

  const chooseLanguage = (nextLanguage: UiLanguage) => {
    void i18n.changeLanguage(nextLanguage)
    setLanguage(nextLanguage)
  }

  return (
    <div aria-label={t('app.language')} className="language-switch" role="group">
      <button
        aria-pressed={language === 'zh'}
        onClick={() => chooseLanguage('zh')}
        type="button"
      >
        中文
      </button>
      <button
        aria-pressed={language === 'en'}
        onClick={() => chooseLanguage('en')}
        type="button"
      >
        EN
      </button>
    </div>
  )
}

export default function App() {
  const { t } = useTranslation()
  const language = useUiStore((state) => state.language)

  useEffect(() => {
    document.title = t('app.pageTitle')
    document
      .querySelector('meta[name="description"]')
      ?.setAttribute('content', t('app.metaDescription'))
  }, [language, t])

  return (
    <div className="app-shell">
      <header className="museum-header">
        <a className="brand-link" href="#/">
          <span className="brand-mark">{t('app.brand')}</span>
          <span className="brand-subtitle">{t('app.subtitle')}</span>
        </a>
        <LanguageSwitch />
      </header>
      <LazyRouteErrorBoundary
        key={language}
        message={t('app.versionPublished')}
        reloadLabel={t('app.reload')}
      >
        <RouterView />
      </LazyRouteErrorBoundary>
    </div>
  )
}

```

### src/ui/story/index.ts

```typescript
export {
  default as ScrollStory,
  STORY_FRAME_EVENT,
  storyStageState,
} from "./ScrollStory";
export type { ScrollStoryProps, StoryStageState, StoryStep } from "./types";

```

### src/ui/compare/index.ts

```typescript
export { default as ComparisonTable } from "./ComparisonTable";
export { default as CompareView } from "./CompareView";
export type { CompareSceneContext } from "./CompareView";
export { CompareGeometryCache, compareGeometryCache } from "./geometryCache";
export {
  createSchemeTransition,
  differencePartIds,
  driveComparedGraphs,
  driveComparedMachineGraphs,
  driveNodeForSpec,
  schemeGhostPresentation,
  specForScheme,
  tintForDifference,
} from "./model";
export type {
  CompareSide,
  SchemeGhostPresentation,
  SchemeTransitionMetadata,
} from "./model";
export { useCompareStore } from "./store";

```

### src/ui/i18n/index.ts

```typescript
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

import en from "./en.json";
import zh from "./zh.json";

export type SupportedLanguage = "en" | "zh";

export function detectBrowserLanguage(
  browserLanguage?: string,
): SupportedLanguage {
  const language =
    browserLanguage ??
    (typeof navigator === "undefined" ? undefined : navigator.language);
  return language?.toLowerCase().startsWith("zh") ? "zh" : "en";
}

export function storedLanguage(): SupportedLanguage | undefined {
  if (typeof localStorage === "undefined") return undefined;
  try {
    const language = localStorage.getItem("mechanica-lang");
    return language === "en" || language === "zh" ? language : undefined;
  } catch {
    return undefined;
  }
}

export const initialLanguage = storedLanguage() ?? detectBrowserLanguage();

void i18n.use(initReactI18next).init({
  fallbackLng: "en",
  interpolation: { escapeValue: false },
  lng: initialLanguage,
  resources: { en: { translation: en }, zh: { translation: zh } },
  supportedLngs: ["en", "zh"],
});

export default i18n;

```

### pnpm-workspace.yaml

```yaml
packages:
  - "."
allowBuilds:
  esbuild: true

```

### vitest.config.ts

```typescript
import { configDefaults, defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    exclude: [...configDefaults.exclude, 'e2e/**'],
    passWithNoTests: true,
  },
});

```

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