@vgai/engine 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.4",
5
+ "version": "0.5.5",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -22,6 +22,7 @@ import type { ResolvedAdapterRoot } from '../manifest/load';
22
22
  import type { GameSession } from './create-runtime';
23
23
  import { type MountEntry, mountManifestRoots, resolveManifest } from './mount-manifest';
24
24
  import type { GameSetupFn } from './types';
25
+ import { assertExportedOrInEditor } from './unexported-game-trap';
25
26
 
26
27
  // ---------------------------------------------------------------------------
27
28
  // The host contract (design/26 §5 D1's `ManifestHost`)
@@ -232,6 +233,11 @@ export async function mountGameFromManifest(
232
233
  host: ManifestHost,
233
234
  opts: MountGameOptions = {},
234
235
  ): Promise<GameSession> {
236
+ // Owner invariant: an unexported game never mounts outside the editor. The
237
+ // editor mounts through its own resolver, so every caller of THIS function
238
+ // is a standalone page — and on a dev server, that page refuses to boot
239
+ // (see unexported-game-trap.ts for why this is a trap and not a rule).
240
+ assertExportedOrInEditor();
235
241
  const manifest = resolveManifest(manifestInput);
236
242
  const entries: Record<string, MountEntry> = { ...opts.entries };
237
243
 
@@ -0,0 +1,82 @@
1
+ /**
2
+ * THE UNEXPORTED-GAME TRAP: a dev-served standalone game page REFUSES to boot.
3
+ *
4
+ * Owner decision (2026-08-09): until a game is EXPORTED, it is never played
5
+ * outside the editor. The editor is the one authoring, viewing and
6
+ * verification surface; the standalone page exists to serve the exported
7
+ * artifact, not to be a second door during development.
8
+ *
9
+ * Why this is a trap in CODE and not a rule in prose: the rule already
10
+ * existed, and it drifted. When an editor session broke mid-task, an agent
11
+ * "temporarily" switched to `npm run game`, the fallback fed its whole
12
+ * feedback loop (pixels for screenshots), nothing broken ever pushed it back,
13
+ * and hours of verification ran outside the editor without anyone deciding
14
+ * that. A workaround whose cost lands outside the loop that chose it is
15
+ * sticky; the only fix that holds is for the workaround PATH ITSELF to fail
16
+ * immediately, loudly, and with the way back in its hands. That is this file.
17
+ *
18
+ * The boundary is DEV-SERVED vs EXPORTED, decided by the bundler: a Vite dev
19
+ * server strips nothing, so `import.meta.env.DEV` is true; a production build
20
+ * (`vite build` — what `vgai deploy` runs) compiles it false and this module
21
+ * costs an exported game nothing. Headless contexts (unit tests, playtest's
22
+ * node leg) have no real browser page and never trip it — the check requires
23
+ * a document whose `defaultView` is the window, which no test stub wires up.
24
+ *
25
+ * There is deliberately NO opt-out flag, env var, or query param. An escape
26
+ * hatch an agent can reach for is the workaround again, one hop later.
27
+ */
28
+
29
+ /** What the refusal says — one place, so the page and the thrown Error agree. */
30
+ const REFUSAL = [
31
+ 'UNEXPORTED GAME, OUTSIDE THE EDITOR — refusing to mount.',
32
+ '',
33
+ 'This game has not been exported. Until it is, the ONLY way to run, see,',
34
+ 'or drive it is the editor:',
35
+ '',
36
+ ' vgai edit . open (or reuse) the editor for this project',
37
+ ' vgai play enter play mode from the terminal',
38
+ " vgai eval '<js>' drive and read the running game",
39
+ '',
40
+ 'The standalone page serves EXPORTED builds only (`vgai deploy`, or the',
41
+ 'production build it runs). Do not script around this page — no headless',
42
+ 'browsers, bots, or screenshots against the dev server. Every agent',
43
+ 'workflow goes through the editor session.',
44
+ ].join('\n');
45
+
46
+ /** True only on a real, dev-served browser page — the one context the trap is for. */
47
+ function isDevServedBrowserPage(): boolean {
48
+ if (typeof window === 'undefined' || typeof document === 'undefined') return false;
49
+ // Test stubs install a bare `document` object; only a real page has the
50
+ // document↔window linkage.
51
+ if (document.defaultView !== window) return false;
52
+ return Boolean(import.meta.env?.DEV);
53
+ }
54
+
55
+ /**
56
+ * Throws (and paints a full-screen refusal, so a human at the tab sees it as
57
+ * immediately as a harness watching the console does) when an unexported game
58
+ * is being mounted on a dev-served page. Called by `mountGameFromManifest`
59
+ * before any root is resolved; exported builds and headless tests pass
60
+ * through untouched.
61
+ */
62
+ export function assertExportedOrInEditor(): void {
63
+ if (!isDevServedBrowserPage()) return;
64
+
65
+ const screen = document.createElement('pre');
66
+ screen.textContent = REFUSAL;
67
+ screen.style.cssText = [
68
+ 'position:fixed',
69
+ 'inset:0',
70
+ 'z-index:2147483647',
71
+ 'margin:0',
72
+ 'padding:48px',
73
+ 'background:#1A0E0E',
74
+ 'color:#FF9B8A',
75
+ 'font:600 15px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace',
76
+ 'white-space:pre-wrap',
77
+ ].join(';');
78
+ document.body.appendChild(screen);
79
+ document.title = 'UNEXPORTED — use the editor';
80
+
81
+ throw new Error(`mountGameFromManifest: ${REFUSAL}`);
82
+ }