# AGENTS.md

## Overview

`www-v` is a Next.js 16 monorepo. It contains a markdown document/post platform and an embedded Three.js isometric 3D engine with Ammo.js physics. It runs on a shared backend (VMAX) and publishes to VMAX.ai.

## Documentation map: READ the matching doc BEFORE editing a subsystem

The detail lives in per-subsystem docs, not here. Each doc is the **contract** for its area. Do not edit a subsystem without reading its doc first.

The harness auto-applies a directory's `AGENTS.md` when you work inside that folder (the built-in enforcement). These are the docs to open up front. Sub-`AGENTS.md` files nest. Always read the **deepest** one that owns your file.

| Read this | Covers | Before you... |
|---|---|---|
| [`www-v/app/AGENTS.md`](www-v/app/AGENTS.md) | the Next.js App Router web front door: route map, the modern-Next.js conventions this repo holds to, and how a post URL becomes a rendered page | touch any route, page, layout, or the SSR pipeline |
| ↳ [`www-v/app/-/AGENTS.md`](www-v/app/-/AGENTS.md) | the `app/-/` standalone reference and test routes (scenes, components, samples) | add or use a test/debug/reference route |
| [`www-v/three-isometric-engine/AGENTS.md`](www-v/three-isometric-engine/AGENTS.md) | the 3D engine: scene architecture, the ocean-ellipsoid placement rules plus cheat-sheet, mount manifest, per-frame loop, shadow scheduling, castle build, shaders, physics | touch anything under `three-isometric-engine/` |
| ↳ [`www-v/three-isometric-engine/world-entities/AGENTS.md`](www-v/three-isometric-engine/world-entities/AGENTS.md) | per-entity contracts (what each publishes/subscribes, its manifest gate, quirks) | edit or add an `Entity` |
| ↳ [`www-v/three-isometric-engine/world-meshes/AGENTS.md`](www-v/three-isometric-engine/world-meshes/AGENTS.md) | per-factory reference (every mesh/geometry factory) | edit or add a mesh factory |
| ↳ [`www-v/three-isometric-engine/agent-simulation/AGENTS.md`](www-v/three-isometric-engine/agent-simulation/AGENTS.md) | scenery/geometry that survived the agent-runtime removal: flag cloth, boat-placement geometry (water-sdf, offshore band), c-space grid | touch flag cloth, boat placement, or wayfinding geometry |
| [`www-v/next-vmax/AGENTS.md`](www-v/next-vmax/AGENTS.md) | per-component invariants of the `next-vmax` library not covered by its skill doc | edit `next-vmax/` |
| ↳ [`www-v/next-vmax/islands/AGENTS.md`](www-v/next-vmax/islands/AGENTS.md) + [`www-v/next-vmax/islands/island-scenes.ts`](www-v/next-vmax/islands/island-scenes.ts) | **The catalog of every scene the engine can build.** One preset row per scene (each worldType, content flag, and showcase); a preset row IS the manifest input. `IslandStage` instances any preset; each island folder keeps its own `AGENTS.md`. Browse a scene at `/-/vmax-tools-template/islands/{key}` | build, change, or verify any island / structure / scene |
| [`www-v/next-vmax-tools/AGENTS.md`](www-v/next-vmax-tools/AGENTS.md) | tools-shell invariants not covered by its skill doc | edit `next-vmax-tools/` |
| [`www-v/document-system-common/AGENTS.md`](www-v/document-system-common/AGENTS.md) | shared non-React document layer (constants, utils, shader-context) | edit `document-system-common/` |
| [`www-v/document-system-components/AGENTS.md`](www-v/document-system-components/AGENTS.md) | markdown-post React UI plus the six overlay shaders | edit `document-system-components/` |
| [`www-v/next-slate-markdown/AGENTS.md`](www-v/next-slate-markdown/AGENTS.md) | Slate.js editor, API client, public-page chrome | edit `next-slate-markdown/` |
| [`www-v/public/SKILL.md`](www-v/public/SKILL.md) | VMAX.ai posting API, Slate JSON serialization, common-mistakes checklist | run any posting workflow |
| [`www-v/ALGORITHMS.md`](www-v/ALGORITHMS.md) | catalogue of every algorithm (noise, terrain, SPH/cloth, shaders, and more) with code demos. **Jump to its Table of Contents section; do not read end-to-end** (116 KB) | implement or modify an algorithm |
| [`www-v/skills/`](www-v/skills) `*/SKILL.md` | task workflows (new island/structure/agent, shader and TypeScript perf discipline, LaTeX to VMAX) | invoke that workflow. Read the one skill fully first |

## Top-level directories

| Directory | Purpose |
|---|---|
| `app/` | Next.js App Router pages (home, profiles, posts, admin) plus the `app/-/` reference/test routes. See [`www-v/app/AGENTS.md`](www-v/app/AGENTS.md). |
| `common/` | Repo-wide shared utilities. `derive-world-mode.ts` (the canonical scene-mode plus world-type rules), `theme-colors.ts` (CSS variable to THREE.Color helpers), `attach-theme-observer.ts` (rAF-coalesced body MutationObserver for theme swaps), `scene-modal-bridge.ts` (`requestSceneModal(key)`: the window-event bridge a Three.js scene element fires to open a React modal it cannot reach directly), `position.ts`, `utilities.ts` (`setTheme`, `toggleFontClassName`), `navigation-links.ts`, `home-post-descriptions.ts`, `research-copy.ts` (the two research/home intro paragraphs and the `firstSentences` helper that derives the `/research` meta description from the first) |
| `components/` | Top-level React components: page chrome (`DocumentLayout`, `DocumentHeading`, `VmaxAppNavigation`, `VmaxAppDocument`, `VmaxAppIsometricBackdrop`, `VmaxAppPaperPostItem`), dropdowns (`DropdownMenu`, `DropdownMenuTrigger`), the modal system (`ModalContext`, `ModalTrigger`, `OutsideElementEvent`, `ModalRenderer`, `SceneModalBridge`; the last two mounted once in `Providers`; the overlay is click-through so a Three.js scene behind stays interactive, and `SceneModalBridge` opens a registered modal when a scene element fires the `scene-modal-bridge` event, deduped), `SignInModalLauncher` (shows `ModalAuthentication` by default on `/sign-in`), `SignInScreen` (the shared sign-in surface that both `/sign-in` and `/admin` render: home-page chrome (`VmaxAppNavigation` + `VmaxAppIsometricBackdrop` with `island="propel"` + `signInVariant`) plus `SignInModalLauncher`; no grid/grain document chrome; both components carry sign-in/sign-up naming, never "login"), tags (`AvatarSocialTag`, `AffiliationSquare`), `ForestCacheWarmer` (idle-time forest template bake), `PasswordProtectedPost`, `VmaxWaitlist` (the home-page `.actions` email form; a client component that calls `HTTP.joinWaitlist({ email })` for the `vmax.ai` domain and shows a uniform confirmation), `ActionListItem`, `DefaultMetaTags`, `NotFoundNotice` (the client `/404` sentence that reads the requested URL from `window.location`), `Providers` |
| `next-vmax/` | The application-component library for `app/-/` routes: the app shell (`DefaultLayout`), windows (`ApplicationWindow`, `WebTerminalWindow`), buttons (`GlassButton`, `ButtonAction`, `ButtonActionGlass`) and the `ButtonLoader` spinner (used by the tools template), navigation (`NavigationTab`, `NavigationDropDownButton`), `CodeBlock`/`InlineCode`, `CylinderOdometer`, `Logo`, `Tooltip`, `Icon`, `Checkbox`, `ScrollSpacer`, `FormTypography`, `ModalAuthentication` (the non-dismissable `/sign-in` Google-auth gate), the `islands/` folder (`island-scenes.ts` preset catalog, `IslandStage` the one component that instances a preset onto the engine, `WorldStage` its distant-islands sibling shared by the home hero and the tools `Distant Islands` scene, and one `AGENTS.md` per island; see [`www-v/next-vmax/islands/AGENTS.md`](www-v/next-vmax/islands/AGENTS.md)), and `graphs/` (reference data for all 15 graph types). Per-component contracts: [`www-v/skills/create-old-vmax-application-ui/SKILL.md`](www-v/skills/create-old-vmax-application-ui/SKILL.md) (deprecated, but the reference for this library). |
| `next-vmax-tools/` | The `/-/vmax-tools-template` tools shell and its `Form*` body kit (alias `@nextjs-vmax-tools/*`): a reference app to clone into a CSS-perfect replica for any internal tool. `AppContainer` is the shell; `tools-pages.tsx` is the `TOOLS_PAGES` registry both routes read (the way `island-scenes.ts` is the one-row-per-scene catalog for islands). The shell, the registry, the `?columns=N` behaviour, and every `Form*` primitive are documented in [`www-v/skills/create-vmax-tools-page/SKILL.md`](www-v/skills/create-vmax-tools-page/SKILL.md). |
| `document-system-common/` | Shared non-React layer for the document platform. Constants, utilities, `shader-context` (shared WebGL2 manager). See [`www-v/document-system-common/AGENTS.md`](www-v/document-system-common/AGENTS.md). |
| `document-system-components/` | React UI for markdown posts. `Document`, `DocumentLayout`, `LatexBlock`, `MermaidGraph`, `BlockImage`, the six overlay shaders, `Providers`. See [`www-v/document-system-components/AGENTS.md`](www-v/document-system-components/AGENTS.md). |
| `next-slate-markdown/` | Slate.js editor plus API client plus public-page chrome. Posts persist as markdown. See [`www-v/next-slate-markdown/AGENTS.md`](www-v/next-slate-markdown/AGENTS.md). |
| `three-isometric-engine/` | Three.js 3D engine with physics, shaders, scenes (CTF mode, wander, island variants). See [`www-v/three-isometric-engine/AGENTS.md`](www-v/three-isometric-engine/AGENTS.md). See [`www-v/skills/fast-threejs-shaders-check/SKILL.md`](www-v/skills/fast-threejs-shaders-check/SKILL.md) for the GPU and GLSL performance discipline. |
| `modules/` | Tiny shared modules (`image-extensions`, `video-extensions`) and vendored third-party code (`minisearch.js` + `minisearch.d.ts`, the full-text search engine, imported as `@modules/minisearch`) |
| `pages/` | Legacy Pages Router (one endpoint remains: `oauth.tsx`) |
| `scripts/` | Build/utility scripts (run via `npm run script`) |
| `public/` | Static assets including `SKILL.md` (the agent posting guide, mirrored at `/skill` and `https://vmax.ai/SKILLS`) |
| `skills/` | Project-local agent skills |

### Cross-file invariants in `common/` and `components/`

- **All internal navigation is a HARD load: never soft-navigate.** No `next/link`, no `useRouter`/`router.push`/`router.replace` anywhere: hrefs render plain `<a>` and programmatic navigation assigns `window.location.href`. Pages here stack full-viewport fixed overlays (scene backdrops, the persistent `ModalRenderer`/`SceneModalBridge` in root `Providers`), so a client-side route transition leaves the previous page's overlays mounted over the next page (e.g. the `/sign-in` auth modal riding into `/`). Same-page URL cleanup that must NOT reload (e.g. stripping seed params after reading them) uses `window.history.replaceState`, which is not a navigation.
- `Providers` seeds `activeConfigs` WITHOUT reading `window.matchMedia` in the `useState` initializer so the server and client produce the same initial render; the mount `useEffect` reconciles the real `prefers-color-scheme`. Do not "fix" the first-paint theme by moving the media query into the initializer.
- `attachThemeObserver` stores its MutationObserver on `mesh.userData.themeObserver`: the key name is a contract with the engine's unmount scan (`three-isometric-engine/common/three/scene-teardown.ts` disconnects observers found under that key); renaming the key leaks observers on teardown.
- `NavigationMenuItem.themeAction` and `authAction` are serializable stand-ins for `onClick`: `DOCUMENT_NAVIGATION_ITEMS` is a static array shared by server and client consumers, where a function would not serialize, so action items carry a token and the client-side dropdown assemblers map it to behaviour via `resolveNavigationItems` (`themeAction` → `Utilities.setTheme`; `authAction: 'sign-out'` → cookie-aware Sign Out / Home). New action-like items must follow the same token pattern, not a callback.
- `home-post-descriptions.ts` resolves each blurb from `PUBLISHED_POSTS` by HREF, never by list index: `PUBLISHED_POSTS` is ordered newest-first while the engine (`GLComponent`) owns the fixed `post_number` → tag mapping (1 → Populora, 2 → UNIX CTF, 3 → PROPEL), so index-based keying would mismatch Populora and PROPEL. The keys are local post_numbers, not real post UUIDs, so copy edits never touch an id; the home page and `/-/vmax-tools-template/islands/distant-islands` both mount the scene from this one source.
- `PUBLISHED_POSTS` (`navigation-links.ts`) is a CURATION list, not a content source: it names which team posts are surfaced and in what order (newest first), and ONLY its `href` is authoritative. Every surface that draws a post's title, date, or abstract now fetches them live through `Server.getResearchPosts()` (see `next-slate-markdown/AGENTS.md`): `/research`, the `/-/vmax-tools-template/research` reference copy, and the home page, where `app/page.tsx` feeds the result to `getHomePostDescriptions(posts)` for the in-scene post tags. `VmaxAppDocument` no longer lists the posts at all: its container ends with a `.actions` column that leads with the `VmaxWaitlist` email form (the `active` primary action, a client component posting to the `vmax.ai` waitlist) over two uncoloured (`tinted`) `GlassButton`s (the Careers button pointing at `CAREERS_URL`, then "Read our papers" pointing at `RESEARCH_URL`), so `/research` is the one place the feed is enumerated and the home page just links to it. `components/VmaxAppPaperPostItem` is left in the tree with no caller. `publishedPostSlug(href)` is the shared href-to-slug reduction, and `STATIC_RESEARCH_POSTS` is the checked-in `ResearchPost` projection used as the API-outage fallback (`VmaxAppDocument` defaults to it, and the tools registry holds it so the row stays synchronously readable). Editing a post's title or abstract on VMAX.ai therefore updates every surface without a deploy. `getHomePostDescriptions` keeps its `PUBLISHED_POSTS` default because `FormIslandScene` is a CLIENT component and calls it with no argument; that one scene still shows the checked-in blurbs.
- `authorSocialLink(name)` (`navigation-links.ts`) resolves a post author to their X URL from `SOCIAL_LINKS`, and returns `null` when there is no entry (`/research` then renders no glyph for that author). It cannot match on the raw string: `SOCIAL_LINKS` holds display names (`Matthew Sargent`, `Augustine Mavor-Parker`) while a post's `data.authors` carries the publication form, with middle names and initials (`Matthew James Sargent`, `Augustine N. Mavor-Parker`) and sometimes an extended surname (`Matthew Daborn-Sargent`). `authorSocialKeys` therefore reduces any name to `first token + last token` (lowercased, periods and apostrophes stripped) and, when the surname is hyphenated, ALSO emits `first token + final hyphen segment`; both the roster and the queried name are reduced the same way and a match on any key wins. That is what links all three spellings of Matthew to one account. The keys stay deliberately narrow (same given name AND same terminal surname token) so a near-miss drops to `null` rather than mislinking a real person to someone else's account; widening the matching is not a safe cleanup.
- `getCSSVariableColor` (`theme-colors.ts`) resolves a CSS variable through a temporary DOM element and `getComputedStyle(...).color` so oklch, hsl, and rgb variable values all normalise to the `rgb(...)` string form; reading the raw variable text instead would hand `THREE.Color` an oklch string it cannot parse.
- `theme-*` and `font-*` body classes are independent axes: `setTheme` swaps only the theme class (a wholesale `body.className` assignment previously wiped the active font class, a fixed regression), and `toggleFontClassName` keeps `font-*` classes mutually exclusive (clearing any other before applying; off restores the default `--font-family` variables).
- `ForestCacheWarmer` must stay mounted in the root `app/layout.tsx`: that is what warms the forest-template cache on EVERY entry path, including direct external navigation straight to `/[username]/[slug]`, so `createNeutralForest()` skips the bake by the time a grass-world post mounts. The warmup is idempotent (short-circuits when the cache is warm), so double mounts and a second call from `GLComponent` are free.
- Outside-click dismissal works THROUGH the modal system's click-through overlay because `OutsideElementEvent` listens on `document`, not on the overlay element; do not reintroduce an overlay `onClick` (it would never fire under `pointer-events: none`) or re-enable pointer events on the overlay to "fix" dismissal.

## Cross-cutting rules

### Change discipline

- Treat the files you read in the current run as the evidence base. If the evidence does not identify a requirement, state the assumption or ask before changing behavior.
- Preserve existing contracts before chasing a local fix. When behavior changes, update the canonical source, the meaningful assertion or verification path, and any affected agent-facing docs together.
- Expand through the repo's extension points: registries, manifests, entities, shared predicates, helpers, and skills. Do not hard-code one-off branches that future features cannot reuse.

### Node and scripts

Node `>=18` (see `package.json`). Test runner is vitest.

| Command | Purpose |
|---|---|
| `npm run dev` | Dev server on port 10003 |
| `npm run build` | Production build |
| `npm start` | Production server on port 10003 |
| `npm test` | Run tests once |
| `npm run test:watch` | Test watch mode |
| `npm run script` | Run one-off `ts-node` scripts in `scripts/` |

### TypeScript

The codebase is pure TypeScript (`strict: true`, `strictNullChecks: true`, `noUncheckedIndexedAccess: true`, `verbatimModuleSyntax: true`, `target: "es2022"`). The only `.js` files are vendored third-party bundles paired with a `.d.ts` stub (so `allowJs: false` still resolves imports to the types while the bundler picks up the runtime `.js`): `three-isometric-engine/vendor/ammo.js` (WASM wrapper) and `modules/minisearch.js` (the MiniSearch full-text engine, kept verbatim from the npm ESM build, comments and `Map` usage included since vendored code is exempt from the repo conventions), plus `scripts/strip-glb.js` (the dependency-free GLB scrubber, not vendored).

- **No `any` at boundaries.** Exported functions, class constructors, and pub/sub channels have typed parameters. Interior logic uses inference.
- **`as const`** on frozen constant objects preserves literal types.
- **`import type`** for type-only imports (enforced by `verbatimModuleSyntax`).
- **Unused parameters** are prefixed with `_` (enforced by `noUnusedParameters`).
- **Private members** use a `__private` prefix with double underscore (`this.__privateHeatField`, not `this._heatField`). Module-scoped private variables use the same convention (`__privateCameraTarget`, not `_cameraTarget`). The `_` prefix is reserved exclusively for unused parameters. See [`www-v/skills/fast-typescript-check/SKILL.md`](www-v/skills/fast-typescript-check/SKILL.md) for the full naming and performance discipline.
- **No `Map` in hot paths.** Use plain objects with string keys and `Record<string, T>` typing. `Map` is permitted only when keys are object references (identity-based lookup).
- **`typeof` checks** against globals (`document`, `window`, `performance`) are hoisted to module-level constants (`const HAS_WINDOW = typeof window !== 'undefined'`).
- **Barrel exports** exist at each engine subsystem root (`agent-simulation/index.ts`, `world-builder/index.ts`, `world-meshes/index.ts`, `world-entities/index.ts`, `physics/index.ts`, `common/index.ts`). Prefer importing from the barrel over deep internal paths.
- **Non-null assertions (`!`)** are used in hot-path engine loops where array indices are bounded by the loop condition. Do not add runtime null guards to these paths.
- **Root `types.d.ts`** globally re-declares Next.js's generated `LayoutProps` (loose `params: Promise<any>` plus a `Record<string, React.ReactNode>` intersection) to work around a Next.js 16 type-generation constraint bug; it looks redundant but the build's generated layout types fail to compile without it; remove only after confirming a Next.js upgrade fixed the generator.

### File naming convention

File names use **hyphen-case** (`shader-context.ts`, `create-palisade-tower.ts`), with two exceptions that use **PascalCase**: React components (`AvatarSocialTag.tsx`, `DefaultLayout.tsx`) and class-per-file modules whose primary export is a single class (`WorldContext.ts`, `GLComponent.ts`, `TeamRegistry.ts`, the `*Entity.ts` files; verified: every PascalCase non-React file in the tree exports a class). camelCase file names are not permitted.

### No comments: knowledge lives in AGENTS.md

**Comments are banned everywhere in this repository.** No `//` line comments, no `/* */` block comments, no `NOTE(@handle)` annotations: in TypeScript, TSX, GLSL, CSS, tests, configs, and scripts alike. Identifier names carry the meaning (see "Identifier naming" below); if code reads unclearly, expand the name or restructure the code; never annotate it. This is permanent policy, not a per-PR preference.

The only permitted exceptions are machine-read directives where technically required: `@ts-expect-error`, `@ts-ignore`, `eslint-disable`, `/// <reference>`. Nothing else survives review.

Knowledge that would once have been a comment (a constraint, a tradeoff, a non-obvious ordering, a workaround, a gotcha) belongs in the deepest `AGENTS.md` that owns the file, as a note that cites the exact function/class/symbol name so a reader can find it from the code (`grep` the symbol, read the doc). If a fact is not worth an AGENTS.md note, it is not worth keeping at all.

The `next-vmax/islands/AGENTS.md` and `world-entities/AGENTS.md` rewrites are the reference for what a good note looks like: every claim traces to live code, names match the source exactly, and nothing is restated that a reader could infer. Match that bar everywhere.

Documentation prose contains no em-dashes, in AGENTS.md files, SKILL.md files, and prose strings in source alike: use a comma, colon, semicolon, parentheses, or a second sentence. Literal em-dash glyphs that are rendered content (the `PostByline` split, the campaign metric placeholder value) are content, not punctuation, and stay.

### Identifier naming: spell names out in full

Names carry the meaning, so they pay for the comment you then do not write. Spell every local variable and parameter out in full: `context` not `ctx`, `deltaX`/`deltaZ` not `dx`/`dz`, `terrainData` not `td`, `event` not `e`, `element` not `el`, `callback` not `cb`, `index` not `i` (and `innerIndex`/a domain name for nested loops). The reader should never decode an abbreviation. The only short names that stay are domain-canonical ones where the full word adds nothing: vector components `x`/`y`/`z` and UV coordinates `u`/`v`.

This rule governs names you own: locals and parameters. It does NOT license churn across boundaries: object/class properties and methods, exported symbols, type and interface fields, JSON manifest keys, pub/sub channel names, React props, THREE `userData` keys, GLSL identifiers (`iTime`, `iFloorBounds`, `uColor`, `vUv`), and CSS variables are contracts: renaming one is an API change, not a readability cleanup, and is out of scope here. Keep the `__private` double-underscore prefix on private members (see TypeScript above).

**Deliberate exceptions (do NOT "fix" these back to longer names).** The codebase was swept to this convention; what stayed short stayed on purpose:
- **Domain-canonical single letters:** `x`/`y`/`z` (coords), `u`/`v` (UV), and `r`/`g`/`b`/`a` ONLY as colour channels. A single letter that is a coordinate, a colour channel, or a generic type parameter (`<T>`) is NOT a loop counter; leave it.
- **`props` and `params`** are kept as canonical React/JS idioms (a maintainer decision), even though they are locals/params.
- **Not variables at all** (so the rule does not apply): `vi.fn()` (the Vitest mock API), CSS `calc(...)` inside style strings, the React/DOM `src` attribute and prop, every other string literal, and any identifier living inside a GLSL/shader template literal (`pos`, `dir`, `mod289`, … inside shader source stay; shaders are excluded wholesale).
- **Type-position contracts:** a function-TYPE signature's parameter name (`decode?: (str: string) => string`) is part of a declared shape, not a binding you own; leave it.

### Brand name casing

Company branding writes the name **Vmax** (capital `V`, lowercase `max`, treated as one word) in prose, UI copy, comments, AND in PascalCase React component names and their file names (`VmaxAppDocument`, `VmaxAppNavigation`, `VmaxAppIsometricBackdrop`, `VmaxAppPaperPostItem`, `Icon.VmaxLogo`). Do NOT write `VMax` (capital `M`) or `VMAX…` inside a mixed-case identifier.

The all-caps **`VMAX`** form survives only where the whole token is uppercase or it is a product/wordmark reference, and stays as-is:

- Product / wordmark / domain in prose and metadata: the `VMAX` backend, `VMAX.ai` / `vmax.ai`, the wordmark SVG.
- `SCREAMING_SNAKE_CASE` constants (`VMAX_SVG_PATHS`, `VMAX_SVG_VIEWBOX`) and data tokens (the `{ VMAX: 'vmax' }` affiliation key), matching the uppercase-constant convention, not a brand-casing choice.
- The lowercase `next-vmax*` folders and `@nextjs-vmax*` aliases.

### Code correctness > tests passing

**The test suite was deliberately RESET** (every `*.test.ts`/`*.test.tsx` deleted; `vitest.config.ts` carries `passWithNoTests` while it is rebuilt). A "pinned by `X.test.ts`" reference in any doc below therefore names an invariant AWAITING re-pinning in the rebuilt suite, not a file in the tree; treat those references as the rebuild checklist. The rules in this section govern the new suite.

**A green `npm test` is not a signal that a feature works.** It only signals that the asserted contracts hold under the harness. Visual correctness, physics plausibility, emergent behavior, cross-subsystem interaction, and "does this look right on a real browser" are out of scope for vitest.

Before reporting a feature complete:

1. For UI / 3D / shader / physics work: run `npm run dev`, load the scene, **look at the result**. A passing test suite with a broken visual is a false green.
2. Prefer asserting invariants (mass conservation, bounded spread, byte-identical non-mutation) over asserting that a function was called. "Runs without throwing" is not a test.
3. When a spec's test matrix asks for a behavioral assertion (e.g., "lava stays in the crater while h < rim") and you weaken it to an API-shape assertion ("height field is the right length"), say so explicitly. Do not let the weaker test stand in for the stronger one.

**Agents (including Claude) are demonstrably weaker at writing tests than at writing implementation code.** Common failure modes:

- Asserting on intermediate state instead of observable behavior.
- Using synthetic inputs that bypass the real bug class.
- Declaring success based on "tests pass" without running the feature.

When delegating test-writing to an agent, check the tests against the spec's intent, not just the spec's wording. When Claude writes a test that passes on the first run without iteration, treat that as a yellow flag. Real tests usually fail once before being right.

**Do not write tests the type system already proves.** A `strict` compiler with `noUncheckedIndexedAccess` is the first test suite; a green `tsc` already guarantees what a runtime assertion would re-check. Delete (and do not add) tests whose only claim is a tautology or a fact the types enforce:

- Default field values of a freshly constructed object (`new Mode()` has empty arrays): the field declarations say so.
- Math identities independent of the code under test (`ceil(sqrt(n))² ≥ n`): that tests the standard library, not us.
- Shape/length/"has property X"/"returns an array" assertions on a typed return: the signature already holds the line.

Keep every test that asserts *behavior the types cannot*: invariants (mass conservation, bounded spread, byte-identical non-mutation), gating and ordering logic, geometry/placement correctness, and regressions. When unsure whether a test is type-covered, keep it: the bar for deletion is "the compiler already fails if this is false."

### Debug visibility rules (apply to every subsystem)

These are three cross-cutting rules. The full contract lives in [`www-v/three-isometric-engine/AGENTS.md`](www-v/three-isometric-engine/AGENTS.md) under "Barrier visibility invariant" and "One-classification-of-truth invariant". This is the summary for contributors working in any directory.

1. **Every barrier renders red.** Hard barriers (physics bodies, planner stamps, predicate gates) AND soft cost-based barriers (RVO penalties, halos, taxes that dominate scoring) MUST draw a red debug primitive. If you cannot draw one, do not stamp it as a barrier. Colour contract:
   - red (`0xff3030`) = physical ground/body barrier
   - blue (`0x3060ff`) = land-agent action/combat circle
   - yellow (`0xfff066`) = wayfinding destination / stake marker
   - green = wayfinding line
2. **Visible matches math.** Every debug overlay's geometry derives from the EXACT constants the runtime predicate uses. No visual-only scale factors, no fudges. The visible IS the gate. Document any clamp / projection in the owning `AGENTS.md`, citing the rendering function.
3. **One classification of truth.** When the same physical concept (sailability, walkability, flammability) is represented in multiple data structures, every representation MUST agree. Multi-class enums NEVER collapse with `> 0` shortcuts. Every consumer lists every class explicitly or imports a canonical predicate next to the source data.

### Path aliases (`tsconfig.json`)

- `@root/*` to `./*`
- `@common/*` to `./common/*`
- `@components/*` to `./components/*`
- `@modules/*` to `./modules/*`
- `@pages/*` to `./pages/*`
- `@engine/*` to `./three-isometric-engine/*`
- `@document-system-common/*` to `./document-system-common/*`
- `@document-system-components/*` to `./document-system-components/*`
- `@nextjs-slate/*` to `./next-slate-markdown/*`
- `@nextjs-vmax/*` to `./next-vmax/*`
- `@nextjs-vmax-tools/*` to `./next-vmax-tools/*`

### Sub-AGENTS files apply hierarchically

The harness auto-applies a sub-AGENTS.md file when an agent is working inside that directory, and they **nest**:

- The four top-level sub-files (`document-system-common`, `document-system-components`, `next-slate-markdown`, `three-isometric-engine`) each own their subsystem in full.
- `three-isometric-engine/` nests further. `three-isometric-engine/world-entities/AGENTS.md` (per-entity contracts) and `three-isometric-engine/world-meshes/AGENTS.md` (per-factory reference) hold the detail that used to bloat the engine root.

**Do not duplicate sub-file content into a parent.** Reference the deepest file that owns the detail.

## Engine data flow

### Entity data-flow diagram

```
Document (Slate.js markdown)
  ↓ deriveWorldMode / deriveWorldType
World Manifest (JSON)
  ↓ mount-world.ts (entity registry + DAG scheduler)
Entities (build → idle bake → update loop)
  ├── OceanBoardEntity → dark + light boards, wake drops
  ├── StarfieldEntity → dark-mode-only full-screen Star Nest sky behind the world
  ├── IslandTerrainEntity → publishLandMass, publishFloorDist
  ├── CastleEntity → publishLandmarks, publishZones
  ├── WalkabilityEntity → publishWalkableOracle
  ├── ForestEntity → publishForestLeaves, onHeatField
  ├── FleetEntity → static offshore rowboat fleet (crews wave in CTF) → publishDockBounds
  ├── GrassBladesEntity → onHeatField, onFloorDist
  ├── VolcanoEntity → publishVolcanoGeometry
  ├── MarbleLavaSystem → publishHeatField
  ├── SmokeFieldEntity → onHeatField, onForestLeaves
  ├── LighthouseFlagEntity → waving flag on each dock lighthouse, claimable by colour
  ├── PalisadeTowerEntity → centre-island watchtower + road; optional coastal dock + recruitment pole
  ├── MotteBaileyCastleEntity → home-page ringwork castle hub; razed by the hwacha
  ├── ShowcaseEntity → mounts one distant-island feature on the centre island for a /-/vmax-tools-template/islands route
  ├── RocketLauncherEntity → "PROPEL" mode: hwacha + village + coastal trebuchet row
  ├── CrystalAgentsEntity → "CRYSTAL" mode: chessboard plinth showcasing the crystal-agent rig
  ├── LumberjackEntity → "Mode:Lumberjack(n)": n coloured crystal-agent lumberjacks fell the forest tree by tree via onStructure tree discs
  ├── DistantIslandsEntity → home-page decorative archipelago; every island wraps to the ocean curve; owns iFloorDistB–K
  └── AirshipEntity → propeller galleon (`home` / `showcase` missions): slung block, drop, land
```

Each entity's full contract, and the catalog of scene variants, lives in [`www-v/three-isometric-engine/world-entities/AGENTS.md`](www-v/three-isometric-engine/world-entities/AGENTS.md) and the `/-/vmax-tools-template/islands` presets. The one-liners above are an index, not the spec.

### How mounting and data flow work

- `mount-world.ts` uses an idle DAG scheduler. Entities declare `deps` in the manifest JSON. The scheduler launches all entries whose deps are satisfied in parallel instead of chaining linearly. Entries with no deps start immediately.
- Cross-entity data flows through `WorldContext` publish/subscribe pairs that replay on subscribe.
- Adding a new scene mode is a new JSON manifest plus a new factory entry, with zero edits inside `firstRender`.

### Scene composition is shared

`common/derive-world-mode.ts` is the single source of truth for scene mode plus world type.

- `deriveWorldMode(events)` returns `{ mode: 'ctf' | 'wander' | 'island', isCTF }` from the byline `EVENT` column.
- `deriveWorldType({ imageCount, isDesert })` returns `'ICE' | 'GRASS' | 'DESERT'`.
- `WorldModeResult.isCTF` is a legacy boolean kept so readers can migrate to `mode === 'ctf'` incrementally; new code reads `mode`, not `isCTF`.
- In `deriveWorldMode`, CTF deliberately takes precedence over wander (`!isCTF && WANDER_RE.test(...)`): a post tagged `CTF | Playtest` can contain a stray "wander" in prose, and the agent system must win. Keep the ordering.

SSR preload (`app/[username]/[slug]/(document)/page.tsx`), the document renderer (`document-system-components/Document.tsx`), and `GLComponent` all read from this one file.

## Publishing to VMAX.ai

Before any posting workflow, read [`www-v/public/SKILL.md`](www-v/public/SKILL.md). It contains the full API reference, Slate JSON serialization rules, and a common-mistakes checklist.

- Posts persist as markdown text (`SAVED_AS_MARKDOWN: true`, post type `NEXT_SLATE_WORLD_MARKDOWN`).
- `::isometric(0)` is auto-prepended by the slug page template.
- `source` field is `'vmax.ai'`.
- Presigned-URL upload domain is `vmax.ai`.

## Agent discovery routes

- `/research` is the public research index (see `app/AGENTS.md`); it is a normal page, not an agent-discovery route, but it is in `sitemap.xml`
- `/agents` serves this file as `text/markdown`
- `/skill` serves `public/SKILL.md` (API posting guide)
- `/skill/convert` serves `skills/tex-or-pdf-to-vmax-markdown/SKILL.md` (LaTeX/PDF/markdown to VMAX conversion guide)
- `/skill/template` serves `skills/create-vmax-tools-page/SKILL.md` (the tools-page building guide)
- `/llms.txt` is a machine-readable summary
- `/sitemap.xml` includes `/agents`, `/skill`, `/skill/convert`, `/skill/template`, `/llms.txt`
- `/robots.txt` allows all paths except `/api/`

The tools sidebar (`SHARED_SIDEBAR_GROUPS` Designer Preview group) links these routes; the site dropdown (`DOCUMENT_NAVIGATION_ITEMS`) no longer links the skill routes; it links `/-/vmax-tools-template` as "System preview" instead. That dropdown's last row is auth-aware: `resolveNavigationItems` resolves the `authAction: 'sign-out'` item against the `vmax` cookie at render time: signed in it becomes "Sign Out" (removes the cookie, redirects `/`, the same flow as the tools sidebar's Sign out row), signed out it becomes a plain "Home" link. Auth-dependent labels must go through `resolveNavigationItems`, never into the static array, because the array is shared by server and client consumers.
