+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/BgmPage.tsx b/apps/web/src/pages/BgmPage.tsx
new file mode 100644
index 0000000..1f50609
--- /dev/null
+++ b/apps/web/src/pages/BgmPage.tsx
@@ -0,0 +1,42 @@
+import { Link } from 'react-router-dom';
+import packages from 'virtual:bgm/packages';
+
+/**
+ * Lists every discovered bgm package. The `virtual:bgm/packages` module's
+ * default export is an array of all packages the loader found in the games
+ * root.
+ */
+export default function BgmPage() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts
index 151aa68..8a8f8a3 100644
--- a/apps/web/src/vite-env.d.ts
+++ b/apps/web/src/vite-env.d.ts
@@ -1 +1,19 @@
-///
\ No newline at end of file
+///
+
+/**
+ * Ambient types for the virtual `bgm` modules served by the bgm vite plugin.
+ *
+ * - `virtual:bgm/packages` — every discovered package.
+ * - `virtual:bgm/package/` — a single package's assembled JSON.
+ */
+declare module 'virtual:bgm/packages' {
+ import type { SerializedPackage } from '@tts/bgm';
+ const packages: SerializedPackage[];
+ export default packages;
+}
+
+declare module 'virtual:bgm/package/*' {
+ import type { SerializedPackage } from '@tts/bgm';
+ const pkg: SerializedPackage;
+ export default pkg;
+}
\ No newline at end of file
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 508f973..436a2ef 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,9 +1,15 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
+import { fileURLToPath } from 'node:url';
+import { bgm } from '@tts/bgm';
export default defineConfig({
- plugins: [react(), tailwindcss()],
+ plugins: [
+ react(),
+ tailwindcss(),
+ bgm({ root: fileURLToPath(new URL('../../games', import.meta.url)) }),
+ ],
server: {
// Proxy API calls to the Hono backend during development.
proxy: {
diff --git a/docs/bgm-loader-status.md b/docs/bgm-loader-status.md
new file mode 100644
index 0000000..a27c91e
--- /dev/null
+++ b/docs/bgm-loader-status.md
@@ -0,0 +1,57 @@
+# bgm Loader — Status
+
+> WIP. What's built, what works, what's missing, and the known issues.
+> Spec: [`bgm-format.md`](./bgm-format.md).
+
+## What's built
+
+### `packages/bgm` — the loader core (new)
+
+| File | Purpose |
+| --- | --- |
+| `src/types.ts` | Roles (`Package`, `Part`, `Surface`, `Setup`, `Route`, `Stacking`, …), `SerializedPackage` (the JSON the plugin emits), `BgmError`, `DefFile`, `ParsedDef` |
+| `src/schemas.ts` | zod schemas + `validate*` for each role |
+| `src/markdown.ts` | Virtual def files from markdown code blocks, via **`marked`**. `file=` naming + content-hash auto-naming (`./.yaml`); multiple blocks may share a `file=` name |
+| `src/parse.ts` | yaml/json/toml → def objects (`yaml`, `smol-toml`); real-file walker (incl. `.csv`) |
+| `src/variants.ts` | `$variants` expansion via **`typed-csv`** (`typed-csv/csv-loader`). Inline (newline) vs path; paths resolve against the virtual def map |
+| `src/collect.ts` | `loadDefs` (real + virtual, virtual wins), `collectPackages` (package decl → `include` globs via **picomatch** → parts/surfaces/setups, `type#id` uniqueness, `$variants` on defs and route candidates) |
+| `src/vite.ts` | The **vite plugin** (`bgm()`): resolves `virtual:bgm/packages` (all packages) and `virtual:bgm/package/` (one package) imports to `export default `, watches source files for reload. Serializes the package's `Map`s to objects (`SerializedPackage`); throws on unknown packages; re-collects per `load` (no stale cache). |
+| `src/*.test.ts` | **23 tests, all passing** — markdown extractor, typed-csv parsing (incl. the spec's empty-array + crop tuple cases), full harbor collection, plugin unit tests (resolve/load/shape/watch), and a real `vite build` integration test covering two packages |
+
+Deps: `marked`, `typed-csv`, `yaml`, `smol-toml`, `picomatch`, `zod`, `vite`, `@types/picomatch`.
+
+### `games/harbor/harbor.md` — example game (new)
+
+Exercises the format end-to-end: package decl, two `file=parts/tokens.yaml` blocks, a surface with `candidates: $variants` against a virtual csv block, and a setup. Same content duplicated as the vitest fixture under `packages/bgm/src/__fixtures__/harbor/`.
+
+### `apps/web` — consumer (new)
+
+- `vite.config.ts` — wired with `bgm({ root: /games })`, importing the plugin from `@tts/bgm`.
+- `src/vite-env.d.ts` — ambient `declare module 'virtual:bgm/packages'` (all packages) and `'virtual:bgm/package/*'` (one package).
+- `src/pages/BgmPage.tsx` — list page: imports `virtual:bgm/packages` and shows every discovered package. Routed at `/bgm`.
+- `src/pages/BgmPackagePage.tsx` — detail page: looks up a package by id from `bgm` and renders its parts/surfaces/setups. Routed at `/bgm/:id`.
+
+The vite plugin itself lives in `packages/bgm/src/vite.ts` (exported from `@tts/bgm`), not in the web app — so the loader's plugin is tested in isolation from the web project.
+
+## Works
+
+- `pnpm --filter @tts/bgm build` and `typecheck` pass.
+- Root `pnpm test`: **177 pass**.
+- `pnpm --filter @tts/web build` succeeds; config warnings fixed.
+- `src/vite.test.ts` runs a **real `vite build`** against a self-contained fixture (`src/__fixtures__/vite-build/`) and asserts the bundled output contains the package data — the plugin is proven end-to-end without touching the web app.
+
+## Known issues
+
+1. ~~Plugin emits empty maps~~ — fixed: `load` serializes `Map`s via `Object.fromEntries`.
+2. ~~Plugin `load` uses `this.error`~~ — fixed: throws instead.
+3. ~~HMR cache not invalidated~~ — fixed: dropped the closure cache; re-collects per `load`.
+4. ~~`tinyglobby` leftover dep~~ — removed.
+5. ~~Package-level test script finds no files~~ — added `packages/bgm/vitest.config.ts`.
+
+## Not yet done
+
+- ~~No consumer yet~~ — `apps/web/src/pages/BgmPage.tsx` lists packages from `virtual:bgm/packages`; `BgmPackagePage.tsx` shows one at `/bgm/:id`.
+- **`$variants` URL paths** — spec mentions file/URL; URLs deferred.
+- **zod `SerializedPackage` shape for the emitted JSON** — the plugin emits `SerializedPackage` objects; a zod schema for the emitted module would give runtime validation beyond the ambient `declare module`.
+- **`setup` value expansion** — `type` without `id` → all parts of that type is documented but not implemented in the loader (it's a game-state init concern; noted as future).
+- Docs for the loader itself (this file is the start).