ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/thatdeveloperguy-website / docs/superpowers/plans/2026-09-15-lobby-hero.md · Lines 1501–2200

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

A continuation of Janady13/thatdeveloperguy-website / docs/superpowers/plans/2026-09-15-lobby-hero.md. The complete source identity, every recorded occurrence, and linked equations are on its first page.

Source transcript · continued

Download complete source text ↓2,200 lines · 121.9 KB
for (const name of ['Ambient', 'DoorOpenIt', 'DoorOpenGovernment', 'DoorOpenCyber', 'FocusIt', 'FocusGovernment', 'FocusCyber']) assert.ok(result.animations.includes(name), `missing animation ${name}`); assert.equal(result.stateMachines.length, 1); for (const name of ['focus', 'openIt', 'openGovernment', 'openCyber', 'reducedMotion']) assert.ok(result.viewModelProperties.includes(name), `missing view-model property ${name}`); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `npm test` Expected: FAIL — `Cannot find module '../../tools/verify-riv.ts'` - [ ] **Step 3: Write `tools/verify-riv.ts`** ```ts import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { repoRoot, type RoomId } from './kits.ts'; const require = createRequire(import.meta.url); /** Loads the runtime file with the low-level Canvas WASM in Node (no DOM) and reads back what the editor exported. */ export async function verifyRiv(room: RoomId) { const dir = resolve(repoRoot, 'scenes/rive', room); const manifest = JSON.parse(readFileSync(resolve(dir, 'rive-manifest.json'), 'utf8')); const bytes = readFileSync(resolve(dir, manifest.file)); if (bytes.subarray(0, 4).toString() !== 'RIVE') throw new Error('not a RIVE file'); const RiveCanvas = (await import('@rive-app/canvas-advanced')).default as (options: { locateFile: (file: string) => string }) => Promise<any>; const rive = await RiveCanvas({ locateFile: file => require.resolve(`@rive-app/canvas-advanced/${file}`) }); const file = await rive.load(new Uint8Array(bytes)); const artboard = file.artboardByName(manifest.artboard); if (!artboard) throw new Error(`artboard ${manifest.artboard} not in ${manifest.file}`); const animations: string[] = []; for (let i = 0; i < artboard.animationCount(); i++) animations.push(artboard.animationByIndex(i).name); const stateMachines: string[] = []; for (let i = 0; i < artboard.stateMachineCount(); i++) stateMachines.push(artboard.stateMachineByIndex(i).name); const viewModel = file.viewModelByName ? file.viewModelByName(manifest.viewModel) : null; const viewModelProperties: string[] = viewModel ? (viewModel.getProperties?.() ?? viewModel.properties ?? []).map((p: any) => p.name) : []; for (const name of manifest.animations) if (!animations.includes(name)) throw new Error(`animation ${name} missing; file has ${animations.join(', ')}`); if (!stateMachines.includes(manifest.stateMachine)) throw new Error(`state machine ${manifest.stateMachine} missing; file has ${stateMachines.join(', ')}`); for (const name of Object.keys(manifest.inputs)) if (!viewModelProperties.includes(name)) throw new Error(`view-model property ${name} missing; file has ${viewModelProperties.join(', ')}`); file.delete?.(); rive.cleanup?.(); return { ok: true as const, artboard: manifest.artboard, animations, stateMachines, viewModelProperties }; } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { const index = process.argv.indexOf('--room'); const result = await verifyRiv((index > 0 ? process.argv[index + 1] : 'lobby') as RoomId); console.log(JSON.stringify(result, null, 2)); } ``` - [ ] **Step 4: Run the test to verify it passes** Run: `npm test` Expected: PASS. If `@rive-app/canvas-advanced` refuses to initialize in Node (`window is not defined`), shim before import — `Object.assign(globalThis, { window: globalThis, self: globalThis })` — and re-run. If `viewModelByName` is absent in 2.42.1, read `file.viewModelCount()` / `file.viewModelByIndex(i)` instead and adapt; the assertion on property names stays. - [ ] **Step 5: Commit** ```bash git add tools/verify-riv.ts tests/unit/verify-riv.test.ts git commit -m "Verify exported .riv files headlessly against their manifest" ``` --- ### Task 9: Truth projection and web app shell **Files:** - Create: `tools/import-truth.ts`, `apps/web/src/truth/projection.json` (generated), `apps/web/src/truth/index.ts`, `apps/web/index.html`, `apps/web/vite.config.ts`, `apps/web/src/main.tsx`, `apps/web/src/App.tsx`, `apps/web/src/components/Header.tsx`, `apps/web/src/components/Footer.tsx`, `apps/web/src/styles/tokens.css`, `apps/web/src/styles/base.css`, `apps/web/src/styles/layout.css`, `apps/web/src/rooms/{it-services,government-solutions,cybersecurity}/Room.tsx`, `apps/web/src/rooms/contact/Contact.tsx` - Test: `tests/unit/truth.test.ts` **Interfaces:** - Produces `projection.json`: ```json { "schema": "tdg-site-truth/v1", "sourceCommit": "<git sha of ~/Code/thatdeveloperguy-truth>", "importedAt": "", "org": { "id": "https://thatdeveloperguy.com/#organization", "name": "ThatDeveloperGuy", "legalName": "THATDEVELOPERGUY LLC", "telephone": "+14176712606", "telephoneDisplay": "", "email": "admin@thatdeveloperguy.com", "foundingDate": "2017", "address": {}, "sameAs": [] }, "person": { "id": "https://thatdeveloperguy.com/#person", "name": "Joseph W. Anady", "jobTitle": "Founder and Principal Engineer", "url": "https://feedthejoe.com/joseph/" }, "host": { "host": "thatdeveloperguy.com", "role": "<properties.hosts[].role verbatim>" }, "ownerToSupply": ["CAGE code", "SAM.gov Active date", "SBA VetCert status", "NAICS approval", "PSC codes", "Named past-performance clients"] } ``` and `export const truth: Projection` from `apps/web/src/truth/index.ts`. - [ ] **Step 1: Write the failing test** `tests/unit/truth.test.ts`: ```ts import { test } from 'node:test'; import assert from 'node:assert/strict'; import { project } from '../../tools/import-truth.ts'; test('project copies node wording verbatim and lists what only the owner can supply', () => { const node = { canonicalHost: 'https://thatdeveloperguy.com/', org: { '@id': 'https://thatdeveloperguy.com/#organization', name: 'ThatDeveloperGuy', legalName: 'THATDEVELOPERGUY LLC', telephone: '+14176712606', telephoneDisplay: '417 671 2606', email: 'admin@thatdeveloperguy.com', foundingDate: '2017', address: { addressLocality: 'Cassville', addressRegion: 'MO' }, sameAs: ['https://www.wikidata.org/wiki/Q139709771'] }, person: { '@id': 'https://thatdeveloperguy.com/#person', name: 'Joseph W. Anady', jobTitle: 'Founder and Principal Engineer', url: 'https://feedthejoe.com/joseph/' }, properties: { hosts: [{ host: 'thatdeveloperguy.com', role: 'the main face' }] } }; const p = project(node as any, 'abc123'); assert.equal(p.schema, 'tdg-site-truth/v1'); assert.equal(p.sourceCommit, 'abc123'); assert.equal(p.org.legalName, 'THATDEVELOPERGUY LLC'); assert.equal(p.org.telephoneDisplay, '417 671 2606'); assert.equal(p.person.jobTitle, 'Founder and Principal Engineer'); assert.equal(p.host.role, 'the main face'); assert.ok(p.ownerToSupply.includes('CAGE code')); assert.equal(JSON.stringify(p).includes('SDVOSB'), false); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `npm test` Expected: FAIL — `Cannot find module '../../tools/import-truth.ts'` - [ ] **Step 3: Write `tools/import-truth.ts`** ```ts import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { repoRoot } from './kits.ts'; export const NODE_ROOT = '/Users/josephanady/Code/thatdeveloperguy-truth'; export const OWNER_TO_SUPPLY = ['CAGE code', 'SAM.gov Active date', 'SBA VetCert status', 'NAICS approval', 'PSC codes', 'Named past-performance clients'] as const; export interface Projection { schema: 'tdg-site-truth/v1'; sourceCommit: string; importedAt: string; org: { id: string; name: string; legalName: string; telephone: string; telephoneDisplay: string; email: string; foundingDate: string; address: Record<string, string>; sameAs: string[] }; person: { id: string; name: string; jobTitle: string; url: string }; host: { host: string; role: string }; ownerToSupply: string[]; } /** Copies fields verbatim; never derives, rewrites or "improves" wording. */ export function project(node: any, sourceCommit: string): Projection { const host = (node.properties?.hosts ?? []).find((h: any) => h.host === 'thatdeveloperguy.com') ?? { host: 'thatdeveloperguy.com', role: '' }; return { schema: 'tdg-site-truth/v1', sourceCommit, importedAt: new Date().toISOString(), org: { id: node.org['@id'], name: node.org.name, legalName: node.org.legalName, telephone: node.org.telephone, telephoneDisplay: node.org.telephoneDisplay ?? '', email: node.org.email, foundingDate: String(node.org.foundingDate ?? ''), address: node.org.address ?? {}, sameAs: node.org.sameAs ?? [] }, person: { id: node.person['@id'], name: node.person.name, jobTitle: node.person.jobTitle, url: node.person.url }, host: { host: host.host, role: host.role }, ownerToSupply: [...OWNER_TO_SUPPLY], }; } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { const node = JSON.parse(readFileSync(resolve(NODE_ROOT, 'organization.json'), 'utf8')); const commit = execFileSync('git', ['-C', NODE_ROOT, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); const out = resolve(repoRoot, 'apps/web/src/truth'); mkdirSync(out, { recursive: true }); writeFileSync(resolve(out, 'projection.json'), JSON.stringify(project(node, commit), null, 2) + '\n'); console.log(`truth projected from ${NODE_ROOT}@${commit.slice(0, 8)}`); } ``` - [ ] **Step 4: Run the test and the import** Run: `npm test && npm run truth && cat apps/web/src/truth/projection.json` Expected: PASS; the JSON shows the org/person fields above with the node's wording. - [ ] **Step 5: Write the app shell** `apps/web/src/truth/index.ts`: ```ts import projection from './projection.json'; export type Projection = typeof projection; export const truth: Projection = projection; ``` `apps/web/index.html`: ```html <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>ThatDeveloperGuy — Government IT. Security. Real solutions.</title> <meta name="description" content="ThatDeveloperGuy LLC — IT services, government solutions and cybersecurity from Cassville, Missouri." /> <link rel="preconnect" href="https://fonts.googleapis.com" /><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;600;700&family=Archivo+Black&display=swap" /> </head> <body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body> </html> ``` `apps/web/vite.config.ts`: ```ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { fileURLToPath } from 'node:url'; export default defineConfig({ root: fileURLToPath(new URL('.', import.meta.url)), plugins: [react()], server: { fs: { allow: [fileURLToPath(new URL('../..', import.meta.url))] } }, build: { outDir: 'dist', emptyOutDir: true, target: 'es2022' }, }); ``` `apps/web/src/main.tsx`: ```tsx import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router'; import { App } from './App'; import './styles/tokens.css'; import './styles/base.css'; import './styles/layout.css'; createRoot(document.getElementById('root')!).render(<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>); ``` `apps/web/src/App.tsx`: ```tsx import { Route, Routes } from 'react-router'; import { Header } from './components/Header'; import { Footer } from './components/Footer'; import { Lobby } from './rooms/lobby/Lobby'; import { Room as ITServices } from './rooms/it-services/Room'; import { Room as GovernmentSolutions } from './rooms/government-solutions/Room'; import { Room as Cybersecurity } from './rooms/cybersecurity/Room'; import { Contact } from './rooms/contact/Contact'; export function App() { return ( <> <Header /> <main id="content"> <Routes> <Route path="/" element={<Lobby />} /> <Route path="/it-services/" element={<ITServices />} /> <Route path="/government-solutions/" element={<GovernmentSolutions />} /> <Route path="/cybersecurity/" element={<Cybersecurity />} /> <Route path="/contact/" element={<Contact />} /> </Routes> </main> <Footer /> </> ); } ``` `apps/web/src/components/Header.tsx`: ```tsx import { Link } from 'react-router'; import { truth } from '../truth'; export function Header() { return ( <header className="site-header"> <Link to="/" className="site-brand" aria-label={`${truth.org.name} — lobby`}>thatdeveloper<span>guy</span>.com</Link> <nav aria-label="Rooms" className="site-nav"> <Link to="/it-services/">IT Services</Link><Link to="/government-solutions/">Government Solutions</Link><Link to="/cybersecurity/">Cybersecurity</Link><Link to="/contact/" className="site-nav-cta">Request a briefing</Link> </nav> </header> ); } ``` `apps/web/src/components/Footer.tsx`: ```tsx import { truth } from '../truth'; export function Footer() { const a = truth.org.address; return ( <footer className="site-footer"> <p><strong>{truth.org.legalName}</strong> · {a.addressLocality}, {a.addressRegion} · <a href={`tel:${truth.org.telephone}`}>{truth.org.telephoneDisplay || truth.org.telephone}</a> · <a href={`mailto:${truth.org.email}`}>{truth.org.email}</a></p> <p className="site-footer-note">Founded {truth.org.foundingDate}. Registry identifiers the owner still has to supply are marked on the pages that need them.</p> </footer> ); } ``` `apps/web/src/styles/tokens.css`: ```css :root { --paper: #f4f1fa; --lavender: #d9d1ea; --purple: #4a2f8a; --purple-deep: #2f1f5e; --ink: #2a1f4a; --status: #33c26b; --font-body: 'Public Sans', system-ui, sans-serif; --font-display: 'Archivo Black', 'Public Sans', sans-serif; --header-h: 64px; --room-w: 1648; --room-h: 928; } ``` `apps/web/src/styles/base.css`: ```css *, *::before, *::after { box-sizing: border-box; } html { color-scheme: light; } body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--font-body); line-height: 1.5; } a { color: var(--purple); } h1, h2, h3 { font-family: var(--font-display); font-weight: 400; letter-spacing: -0.01em; line-height: 1.1; margin: 0 0 .5em; } :focus-visible { outline: 3px dashed var(--purple); outline-offset: 3px; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } ``` `apps/web/src/styles/layout.css`: ```css .site-header { position: sticky; top: 0; z-index: 10; height: var(--header-h); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: rgba(244, 241, 250, .92); backdrop-filter: blur(8px); border-bottom: 1px solid var(--lavender); } .site-brand { font-family: var(--font-display); font-size: 1.15rem; color: var(--ink); text-decoration: none; } .site-brand span { color: var(--purple); } .site-nav { display: flex; gap: 20px; } .site-nav a { text-decoration: none; font-weight: 600; } .site-nav-cta { padding: 8px 14px; border-radius: 999px; background: var(--purple); color: #fff !important; } .room-overview { max-width: 1100px; margin: 0 auto; padding: 56px 24px; display: grid; gap: 32px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } .owner-to-supply { border: 2px dashed var(--purple); border-radius: 8px; padding: 12px 16px; background: #fff; } .site-footer { padding: 32px 24px; border-top: 1px solid var(--lavender); font-size: .95rem; } .site-footer-note { color: #5c4d85; } @media (max-width: 767px) { .site-nav { display: none; } } ``` Placeholder rooms — `apps/web/src/rooms/it-services/Room.tsx` (and the same file body with the title changed for `government-solutions` → "Government Solutions", `cybersecurity` → "Cybersecurity"): ```tsx import { Link } from 'react-router'; export function Room() { return ( <section className="room-overview" aria-labelledby="room-title"> <div><h1 id="room-title">IT Services</h1><p>This room is built in the next plan. <Link to="/">Return to the lobby</Link>.</p></div> </section> ); } ``` `apps/web/src/rooms/contact/Contact.tsx`: ```tsx import { useSearchParams } from 'react-router'; import { truth } from '../../truth'; export function Contact() { const [params] = useSearchParams(); const project = params.get('project') ?? 'general'; return ( <section className="room-overview" aria-labelledby="contact-title"> <div> <h1 id="contact-title">Request a briefing</h1> <p>Intent: <code>{project}</code>. The mail service is wired in the next plan; until then call <a href={`tel:${truth.org.telephone}`}>{truth.org.telephoneDisplay || truth.org.telephone}</a> or write to <a href={`mailto:${truth.org.email}`}>{truth.org.email}</a>.</p> </div> </section> ); } ``` - [ ] **Step 6: Typecheck and start the dev server once** Run: `npm run typecheck && (npm run dev & sleep 4; curl -s http://127.0.0.1:4410/ | head -5; kill %1)` Expected: no type errors (the lobby import will fail until Task 10 — create `apps/web/src/rooms/lobby/Lobby.tsx` exporting `export function Lobby() { return <p>lobby</p>; }` for now); the HTML shell is served. - [ ] **Step 7: Commit** ```bash git add tools/import-truth.ts tests/unit/truth.test.ts apps/web git commit -m "Truth projection from the node; React app shell, routes and tokens" ``` --- ### Task 10: The stage — poster, Rive, hit shapes, caption **Files:** - Create: `scripts/sync-rive-wasm.mjs`, `scripts/sync-scenes.mjs`, `apps/web/src/animation/rive/RiveScene.tsx`, `apps/web/src/animation/stage/SceneStage.tsx`, `apps/web/src/styles/stage.css`, `apps/web/src/rooms/lobby/hotspots.ts`, `apps/web/src/rooms/lobby/content.ts`, `apps/web/src/rooms/lobby/Lobby.tsx` - Test: `tests/unit/hotspots.test.ts` **Interfaces:** - `RiveScene` props: `{ src: string; artboard: string; stateMachine: string; viewModel: string; focus: string; reducedMotion: boolean; fire: { name: string; nonce: number } | null; onReady(): void; onError(error: unknown): void }` — writes `focus` to the enum property whenever it changes, sets `reducedMotion`, fires the trigger named `fire.name` each time `fire.nonce` changes. - `SceneStage` props: `{ poster: string; rive: { src: string; artboard: string; stateMachine: string; viewModel: string } | null; hotspots: Hotspot[]; caption: (id: string | null) => string; onActivate(h: Hotspot): Promise<void> | void; label: string }` where `interface Hotspot { id: string; label: string; href: string; hit: { x: number; y: number; width: number; height: number }; focusValue: string; trigger: string }`. - `hotspots.ts`: `export const LOBBY_HOTSPOTS: Hotspot[]` derived from `scenes/rive/lobby/rive-manifest.json` `doors[]` at import time. - `sync-scenes.mjs` copies `scenes/refined/lobby/scene.svg` → `apps/web/public/scenes/lobby/scene.svg` and `scenes/rive/lobby/lobby.riv` + `rive-manifest.json` → `apps/web/public/scenes/lobby/`; `sync-rive-wasm.mjs` copies the version-matched `@rive-app/webgl2` `rive.wasm` to `apps/web/public/animation/runtime/rive.wasm` (the TSC script, path adjusted). - [ ] **Step 1: Write the failing test** `tests/unit/hotspots.test.ts`: ```ts import { test } from 'node:test'; import assert from 'node:assert/strict'; import { hotspotsFromManifest } from '../../apps/web/src/rooms/lobby/hotspots.ts'; test('lobby hotspots come from the rive manifest doors, with focus values and triggers', () => { const list = hotspotsFromManifest({ doors: [{ id: 'it', leafId: 'door_it_leaf', hinge: [564, 529], hit: { x: 542, y: 229, width: 176, height: 302 }, href: '/it-services/', label: 'IT SERVICES' }] } as any); assert.deepEqual(list, [{ id: 'it', label: 'IT SERVICES', href: '/it-services/', hit: { x: 542, y: 229, width: 176, height: 302 }, focusValue: 'it', trigger: 'openIt' }]); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `npm test` Expected: FAIL — `Cannot find module '../../apps/web/src/rooms/lobby/hotspots.ts'` - [ ] **Step 3: Write the sync scripts and run them** `scripts/sync-rive-wasm.mjs`: ```js import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { existsSync, readFileSync, copyFileSync, mkdirSync, writeFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; process.chdir(fileURLToPath(new URL('..', import.meta.url))); const require = createRequire(import.meta.url); const reactEntry = require.resolve('@rive-app/react-webgl2'); const runtimeEntry = createRequire(reactEntry).resolve('@rive-app/webgl2'); let dir = dirname(runtimeEntry); while (!existsSync(join(dir, 'package.json'))) { const parent = dirname(dir); if (parent === dir) throw new Error('runtime package root not found'); dir = parent; } const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); if (pkg.name !== '@rive-app/webgl2') throw new Error('unexpected runtime package'); const out = 'apps/web/public/animation/runtime'; mkdirSync(out, { recursive: true }); copyFileSync(join(dir, 'rive.wasm'), join(out, 'rive.wasm')); writeFileSync(join(out, 'version.json'), JSON.stringify({ name: pkg.name, version: pkg.version, wasmSHA256: createHash('sha256').update(readFileSync(join(dir, 'rive.wasm'))).digest('hex') }, null, 2)); console.log(`self-hosted Rive WebGL2 runtime ${pkg.version}`); ``` `scripts/sync-scenes.mjs`: ```js import { copyFileSync, mkdirSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; process.chdir(fileURLToPath(new URL('..', import.meta.url))); for (const room of ['lobby']) { const out = `apps/web/public/scenes/${room}`; mkdirSync(out, { recursive: true }); copyFileSync(`scenes/refined/${room}/scene.svg`, `${out}/scene.svg`); for (const file of [`${room}.riv`, 'rive-manifest.json']) if (existsSync(`scenes/rive/${room}/${file}`)) copyFileSync(`scenes/rive/${room}/${file}`, `${out}/${file}`); console.log(`synced ${room}`); } ``` Run: `npm run sync` Expected: both scripts print; `apps/web/public/scenes/lobby/{scene.svg,lobby.riv,rive-manifest.json}` and `apps/web/public/animation/runtime/rive.wasm` exist. - [ ] **Step 4: Write `hotspots.ts`, `content.ts`, `RiveScene.tsx`, `SceneStage.tsx`, `stage.css`, `Lobby.tsx`** `apps/web/src/rooms/lobby/hotspots.ts`: ```ts import manifest from '../../../../../scenes/rive/lobby/rive-manifest.json'; export interface Hotspot { id: string; label: string; href: string; hit: { x: number; y: number; width: number; height: number }; focusValue: string; trigger: string } interface DoorRecord { id: string; leafId: string; hinge: [number, number]; hit: Hotspot['hit']; href: string; label: string } const TITLE: Record<string, string> = { it: 'It', government: 'Government', cyber: 'Cyber' }; export function hotspotsFromManifest(m: { doors: DoorRecord[] }): Hotspot[] { return m.doors.map(door => ({ id: door.id, label: door.label, href: door.href, hit: door.hit, focusValue: door.id, trigger: `open${TITLE[door.id] ?? door.id}` })); } export const LOBBY_HOTSPOTS = hotspotsFromManifest(manifest as any); export const LOBBY_RIVE = { src: `/scenes/lobby/${(manifest as any).file}`, artboard: (manifest as any).artboard as string, stateMachine: (manifest as any).stateMachine as string, viewModel: (manifest as any).viewModel as string }; ``` `apps/web/src/rooms/lobby/content.ts`: ```ts /** Lobby copy. Wording that describes the organization comes from the node projection; these are room captions only. */ export const LOBBY_CAPTIONS: Record<string, string> = { it: 'IT Services — support, endpoints, infrastructure and deployment.', government: 'Government Solutions — planning, requirements, contracting and delivery.', cyber: 'Cyber Security — monitoring, review, response and infrastructure protection.', }; export const LOBBY_REST_CAPTION = 'Three doors. Pick the room you need.'; ``` `apps/web/src/animation/rive/RiveScene.tsx`: ```tsx import { useEffect, useRef } from 'react'; import { useRive, Layout, Fit, Alignment, RuntimeLoader, useViewModel, useViewModelInstance, useViewModelInstanceEnum, useViewModelInstanceBoolean, useViewModelInstanceTrigger } from '@rive-app/react-webgl2'; if (typeof window !== 'undefined') { RuntimeLoader.setWasmUrl('/animation/runtime/rive.wasm'); RuntimeLoader.setWasmFallbackUrl(null); } export interface RiveSceneProps { src: string; artboard: string; stateMachine: string; viewModel: string; focus: string; reducedMotion: boolean; fire: { name: string; nonce: number } | null; onReady: () => void; onError: (error: unknown) => void } /** Plays a room's state machine and mirrors page state into its view model. Pointer events never reach this canvas; the HTML overlay owns them. */ export function RiveScene({ src, artboard, stateMachine, viewModel, focus, reducedMotion, fire, onReady, onError }: RiveSceneProps) { const callbacks = useRef({ onReady, onError }); callbacks.current = { onReady, onError }; const { rive, RiveComponent } = useRive({ src, artboard, stateMachines: stateMachine, autoplay: true, autoBind: true, enableRiveAssetCDN: false, shouldDisableRiveListeners: true, layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }), onLoad: () => callbacks.current.onReady(), onLoadError: e => callbacks.current.onError(e) }, { shouldResizeCanvasToContainer: true, useDevicePixelRatio: true }); const model = useViewModel(rive, { name: viewModel }); const instance = useViewModelInstance(model, { rive }); const focusProperty = useViewModelInstanceEnum('focus', instance); const reduced = useViewModelInstanceBoolean('reducedMotion', instance); const trigger = useViewModelInstanceTrigger(fire?.name ?? 'openIt', instance); useEffect(() => { focusProperty.setValue(focus); }, [focus, focusProperty.setValue]); useEffect(() => { reduced.setValue(reducedMotion); }, [reducedMotion, reduced.setValue]); useEffect(() => { if (fire) trigger.trigger(); }, [fire?.nonce]); return <div className="stage-rive" aria-hidden="true"><RiveComponent /></div>; } ``` Before relying on the hook names, run `grep -o "export declare function useViewModel[A-Za-z]*" node_modules/@rive-app/react-webgl2/dist/types/index.d.ts` (or the package README) and match exactly; if `useViewModelInstanceTrigger` needs `{ onTrigger }`, keep the call shape it documents. `apps/web/src/animation/stage/SceneStage.tsx`: ```tsx import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router'; import { RiveScene } from '../rive/RiveScene'; import type { Hotspot } from '../../rooms/lobby/hotspots'; import '../../styles/stage.css'; export interface SceneStageProps { poster: string; rive: { src: string; artboard: string; stateMachine: string; viewModel: string } | null; hotspots: Hotspot[]; caption: (id: string | null) => string; onActivate: (h: Hotspot) => Promise<void> | void; label: string; focus: string | null; onFocus: (id: string | null) => void; fire: { name: string; nonce: number } | null } /** 1648×928 room coordinates mapped to the stage; poster underneath, Rive on top, real links on top of that. */ export function SceneStage({ poster, rive, hotspots, caption, onActivate, label, focus, onFocus, fire }: SceneStageProps) { const [ready, setReady] = useState(false); const [failed, setFailed] = useState(false); const [reducedMotion, setReducedMotion] = useState(false); useEffect(() => { const m = matchMedia('(prefers-reduced-motion: reduce)'); const u = () => setReducedMotion(m.matches); u(); m.addEventListener('change', u); return () => m.removeEventListener('change', u); }, []); const activate = useCallback((event: React.MouseEvent, h: Hotspot) => { event.preventDefault(); void onActivate(h); }, [onActivate]); return ( <section className={`stage${ready ? ' stage-live' : ''}`} aria-label={label} data-focus={focus ?? ''}> <div className="stage-frame"> <img className="stage-poster" src={poster} alt="" width={1648} height={928} decoding="async" /> {rive && !failed && !reducedMotion && <RiveScene {...rive} focus={focus ?? 'none'} reducedMotion={reducedMotion} fire={fire} onReady={() => setReady(true)} onError={() => setFailed(true)} />} <svg className="stage-hits" viewBox="0 0 1648 928" role="navigation" aria-label={`${label} destinations`}> {hotspots.map(h => ( <Link key={h.id} to={h.href} aria-label={h.label} className="stage-hit" data-hit={h.id} onMouseEnter={() => onFocus(h.id)} onMouseLeave={() => onFocus(null)} onFocus={() => onFocus(h.id)} onBlur={() => onFocus(null)} onClick={e => activate(e, h)}> <rect x={h.hit.x} y={h.hit.y} width={h.hit.width} height={h.hit.height} rx={6} /> </Link> ))} </svg> </div> <p className="stage-caption" aria-live="polite">{caption(focus)}</p> </section> ); } ``` `react-router`'s `Link` renders an `<a>`; inside an SVG it must be an SVG anchor. If React warns about `<a>` inside `<svg>`, replace `Link` with a plain `<a href={h.href} …>` and call `navigate(h.href)` from `onActivate`. `apps/web/src/styles/stage.css`: ```css .stage { position: relative; min-height: calc(100svh - var(--header-h)); display: grid; grid-template-rows: 1fr auto; background: var(--lavender); } .stage-frame { position: relative; aspect-ratio: 1648 / 928; max-height: calc(100svh - var(--header-h) - 48px); margin: 0 auto; width: min(100%, calc((100svh - var(--header-h) - 48px) * 1648 / 928)); overflow: hidden; } .stage-poster, .stage-rive, .stage-hits { position: absolute; inset: 0; width: 100%; height: 100%; } .stage-poster { display: block; object-fit: cover; } .stage-rive { opacity: 0; transition: opacity .4s ease; pointer-events: none; } .stage-live .stage-rive { opacity: 1; } .stage-rive canvas { width: 100% !important; height: 100% !important; display: block; } .stage-hits { z-index: 2; } .stage-hit { cursor: pointer; } .stage-hit rect { fill: transparent; stroke: transparent; stroke-width: 3; stroke-dasharray: 8 6; } .stage-hit:focus-visible { outline: none; } .stage-hit:focus-visible rect { stroke: var(--purple); } .stage-caption { margin: 0; padding: 12px 24px; text-align: center; font-weight: 600; color: var(--purple-deep); min-height: 48px; } @media (max-width: 767px) { .stage-frame { max-height: none; width: 100%; } .stage-rive { display: none; } } ``` `apps/web/src/rooms/lobby/Lobby.tsx`: ```tsx import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router'; import { SceneStage } from '../../animation/stage/SceneStage'; import { LOBBY_HOTSPOTS, LOBBY_RIVE, type Hotspot } from './hotspots'; import { LOBBY_CAPTIONS, LOBBY_REST_CAPTION } from './content'; import { truth } from '../../truth'; const DOOR_OPEN_MS = 26 / 60 * 1000 + 120; export function Lobby() { const navigate = useNavigate(); const [focus, setFocus] = useState<string | null>(null); const [fire, setFire] = useState<{ name: string; nonce: number } | null>(null); const [leaving, setLeaving] = useState(false); const activate = useCallback(async (h: Hotspot) => { if (leaving) return; setLeaving(true); setFocus(h.id); setFire({ name: h.trigger, nonce: Date.now() }); const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; await new Promise(r => setTimeout(r, reduced ? 0 : DOOR_OPEN_MS)); navigate(h.href); }, [leaving, navigate]); return ( <> <h1 className="sr-only">{truth.org.name} — {truth.host.role}</h1> <SceneStage poster="/scenes/lobby/scene.svg" rive={LOBBY_RIVE} hotspots={LOBBY_HOTSPOTS} label="Lobby" focus={focus} onFocus={id => { if (!leaving) setFocus(id); }} fire={fire} onActivate={activate} caption={id => (id ? LOBBY_CAPTIONS[id] ?? '' : LOBBY_REST_CAPTION)} /> <section className="room-overview" aria-label="About this organization"> <div><h2>{truth.org.name}</h2><p>{truth.org.legalName}, founded {truth.org.foundingDate}, {truth.org.address.addressLocality}, {truth.org.address.addressRegion}.</p><p>{truth.person.name}, {truth.person.jobTitle}.</p></div> <div className="owner-to-supply"><h2>Registry facts to be supplied by the owner</h2><ul>{truth.ownerToSupply.map(item => <li key={item}>{item}</li>)}</ul></div> </section> </> ); } ``` - [ ] **Step 5: Run the unit tests, typecheck, and look at it** Run: `npm test && npm run typecheck && npm run dev` Then open `http://127.0.0.1:4410/` in Chrome. Expected: header, the refined lobby as poster, then the Rive canvas fading in over it (same geometry — no jump), the caption changing on hover over each door, dashed focus ring on Tab, and a click on a door opening it and landing on the placeholder room. Check the console for Rive load errors; if `useViewModelInstanceEnum` rejects the value, confirm the enum value keys in `rive-manifest.json` (`none`, `it`, `government`, `cyber`). - [ ] **Step 6: Commit** ```bash git add scripts apps/web/src tests/unit/hotspots.test.ts git commit -m "Scene stage: poster, Rive view-model bridge, hit-shape links, caption; lobby route" ``` --- ### Task 11: Browser tests and fps measurement **Files:** - Create: `playwright.config.ts`, `tests/browser/lobby.spec.ts`, `tools/shots.ts` - [ ] **Step 1: Write the Playwright config and the failing spec** `playwright.config.ts`: ```ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: 'tests/browser', timeout: 30_000, retries: 0, reporter: 'list', use: { baseURL: 'http://127.0.0.1:4410', trace: 'retain-on-failure' }, webServer: { command: 'npm run dev', url: 'http://127.0.0.1:4410', reuseExistingServer: true, timeout: 60_000 }, projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'mobile', use: { ...devices['iPhone 14'] } }], }); ``` `tests/browser/lobby.spec.ts`: ```ts import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('the lobby shows the poster and three real door links', async ({ page }) => { await page.goto('/'); await expect(page.locator('.stage-poster')).toBeVisible(); const doors = page.locator('.stage-hit'); await expect(doors).toHaveCount(3); await expect(doors.nth(0)).toHaveAttribute('href', '/it-services/'); await expect(doors.nth(1)).toHaveAttribute('href', '/government-solutions/'); await expect(doors.nth(2)).toHaveAttribute('href', '/cybersecurity/'); }); test('hovering a door names it in the caption; keyboard reaches every door', async ({ page, isMobile }) => { test.skip(isMobile, 'hover is desktop-only'); await page.goto('/'); await page.locator('.stage-hit[data-hit="government"]').hover(); await expect(page.locator('.stage-caption')).toContainText('Government Solutions'); await page.keyboard.press('Tab'); // brand for (let i = 0; i < 6; i++) { await page.keyboard.press('Tab'); if (await page.locator('.stage-hit:focus').count()) break; } await expect(page.locator('.stage-hit:focus')).toHaveCount(1); }); test('clicking the IT door opens it and navigates', async ({ page }) => { await page.goto('/'); await page.locator('.stage-hit[data-hit="it"]').click(); await expect(page).toHaveURL(/\/it-services\/$/, { timeout: 5000 }); await expect(page.getByRole('heading', { level: 1 })).toContainText('IT Services'); }); test('the lobby has no axe violations', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations, JSON.stringify(results.violations, null, 1)).toEqual([]); }); ``` - [ ] **Step 2: Install browsers and run the spec** Run: `npx playwright install chromium webkit && npm run test:browser` Expected: all four tests pass on chromium and webkit, the first/third/fourth on mobile. Fix real failures in the app, not the tests; an axe "region" complaint on the stage means the `<section aria-label>` is missing. - [ ] **Step 3: Write `tools/shots.ts`** ```ts /** Frames and fps of `/` in headless Chrome with the real GPU: node --import tsx tools/shots.ts [--cpu 4] */ import { spawn } from 'node:child_process'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const arg = (k: string, d: string) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1]! : d; }; const BASE = arg('--base', 'http://127.0.0.1:4410'), CPU = Number(arg('--cpu', '1')), OUT = arg('--out', 'shots/lobby'), W = 1440, H = 900; const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; if (!existsSync(CHROME)) throw new Error('Google Chrome not found'); mkdirSync(OUT, { recursive: true }); const profile = mkdtempSync(join(tmpdir(), 'tdg-shots-')); const chrome = spawn(CHROME, ['--headless=new', '--remote-debugging-port=0', `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check', `--window-size=${W},${H}`, '--hide-scrollbars', '--use-angle=metal', '--enable-gpu-rasterization', '--ignore-gpu-blocklist'], { stdio: 'ignore' }); const portFile = join(profile, 'DevToolsActivePort'); for (let i = 0; i < 100 && !existsSync(portFile); i++) await new Promise(r => setTimeout(r, 100)); const port = readFileSync(portFile, 'utf8').split('\n')[0]!.trim(); const target = await (await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' })).json() as { webSocketDebuggerUrl: string }; const ws = new WebSocket(target.webSocketDebuggerUrl); await new Promise<void>((res, rej) => { ws.addEventListener('open', () => res()); ws.addEventListener('error', rej); }); let id = 0; const waits = new Map<number, (v: any) => void>(); ws.addEventListener('message', e => { const m = JSON.parse(String(e.data)); if (m.id && waits.has(m.id)) { waits.get(m.id)!(m); waits.delete(m.id); } }); const send = (method: string, params: Record<string, unknown> = {}) => { const n = ++id; ws.send(JSON.stringify({ id: n, method, params })); return new Promise<any>(r => waits.set(n, r)); }; const ev = async (expr: string) => (await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })).result?.result?.value; const shot = async (name: string) => { const r = await send('Page.captureScreenshot', { format: 'jpeg', quality: 85 }); writeFileSync(join(OUT, `${name}.jpg`), Buffer.from(r.result.data, 'base64')); }; const wait = (ms: number) => new Promise(r => setTimeout(r, ms)); await send('Page.enable'); await send('Runtime.enable'); await send('Emulation.setDeviceMetricsOverride', { width: W, height: H, deviceScaleFactor: 1, mobile: false }); await send('Emulation.setCPUThrottlingRate', { rate: CPU }); await send('Page.navigate', { url: `${BASE}/` }); await wait(1500); await shot('00-arrive'); await wait(1500); await shot('01-live'); const renderer = await ev(`(() => { const c = document.createElement('canvas'); const g = c.getContext('webgl2'); const d = g && g.getExtension('WEBGL_debug_renderer_info'); return d ? g.getParameter(d.UNMASKED_RENDERER_WEBGL) : 'no webgl2'; })()`); const live = await ev(`document.querySelector('.stage-live') !== null`); await ev(`document.querySelector('.stage-hit[data-hit="government"]').dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))`); await wait(400); await shot('02-focus-government'); const fps = await ev(`(async () => { const t0 = performance.now(); let f = 0; await new Promise(r => { const tick = () => { f++; if (performance.now() - t0 < 2000) requestAnimationFrame(tick); else r(); }; requestAnimationFrame(tick); }); return Math.round(f / ((performance.now() - t0) / 1000)); })()`); const result = { renderer, riveLive: live, cpu: CPU, fpsIdle: fps, at: new Date().toISOString() }; writeFileSync(join(OUT, `fps-${CPU}x.json`), JSON.stringify(result, null, 2)); console.log(JSON.stringify(result)); ws.close(); chrome.kill(); await new Promise(r => chrome.once('exit', r)); rmSync(profile, { recursive: true, force: true }); if (!live) { console.error('Rive did not go live in headless Chrome'); process.exit(1); } if (fps < (CPU >= 4 ? 30 : 55)) { console.error(`fps ${fps} below the bar`); process.exit(1); } ``` - [ ] **Step 4: Measure at 1× and 4×** Run: `npm run dev & sleep 4; npm run shots; npm run shots -- --cpu 4; kill %1` Expected: `riveLive: true`, `fpsIdle ≥ 55` at 1× and `≥ 30` at 4×; three JPEGs in `shots/lobby/`. If fps is below the bar, the refined scene is too heavy for the runtime: raise `DEFAULT_REFINE.tolerance` to 1.0 and `minArea` to 6, re-run `npm run refine -- --room lobby`, `npm run rive:author -- --room lobby --replace`, `npm run rive:verify`, `npm run sync`, and measure again. Record the final numbers in the commit message. - [ ] **Step 5: Commit** ```bash git add playwright.config.ts tests/browser/lobby.spec.ts tools/shots.ts git commit -m "Browser tests (nav, keyboard, axe) and headless fps measurement for the lobby" ``` --- ### Task 12: Hand-off for the owner's verdict - [ ] **Step 1: Run the full verification in order** Run: `npm test && npm run typecheck && npm run rive:verify && npm run test:browser` Expected: all green. - [ ] **Step 2: Start the dev server for Joseph and report** Run: `npm run dev` Report in chat (no report file): the URL `http://127.0.0.1:4410/`, before→after vertex counts and diff ratio from `scenes/refined/lobby/manifest.json`, `lobby.riv` size, state-machine/input names from `rive-manifest.json`, fps at 1× and 4×, the editor URL of the file, and what Plan 2 covers (three rooms, mascot rig, walk-through, contact API, prerender, release). Do not start Plan 2 until Joseph has judged the lobby in Chrome. --- ## Self-review - Spec coverage: repo layout (T1, T9), vector pipeline steps 1–6 (T3–T5), Rive authoring steps 1–7 for the lobby (T7), headless verification (T8), truth projection + owner placeholders (T9), stage with poster/Rive/hit shapes/caption, door click → trigger → navigate, reduced motion, mobile poster (T10), browser gates + fps (T11). Mascot rig, rooms 2–4, contact API, prerender, release: Plan 2 by design. - Type consistency: `RefinedManifest` (T5) is consumed by `planLobby` (T7); `Hotspot` (T10) is produced by `hotspotsFromManifest` and consumed by `SceneStage`; `rive-manifest.json` fields written in T7 are read in T8 and T10 with the same names (`file`, `artboard`, `stateMachine`, `viewModel`, `inputs`, `animations`, `doors`). - Known editor-shape uncertainty is isolated in `rive-author.ts` accessors (ids under `id`/`nodeId`/`group.id`, `properties`/`viewModelProperties`, `animations`/`linearAnimations`) and in the `@rive-app/react-webgl2` view-model hook names; both tasks say to confirm against the live tool result / package types before trusting the code.

Search equations, builds, manuscript text, and recovered sources.