This is the abridged developer documentation for declarative-hex-worlds # declarative-hex-worlds > Declarative, deterministic hex worlds for TypeScript games. Bootstrap KayKit assets in one command. First-class React + Three.js bindings. ## What you ship [Section titled “What you ship”](#what-you-ship) Declarative API Describe what you want — a harbor with three depth tiers, a procedural forest, a road network connecting both. The library handles tile selection, connectivity, prop scatter, and validation. No imperative grid bookkeeping. Deterministic by construction Same seed, same output — byte-identical, across processes and platforms. Server-authoritative simulation, save-game replays, and cross-machine reproducibility all work out of the box. First-class React + Three.js Not optional peer-deps. The library tests against the versions it ships and exposes idiomatic hooks and scene helpers. Install one package and start rendering. ## Bootstrap in three steps [Section titled “Bootstrap in three steps”](#bootstrap-in-three-steps) 1. **Install the package** ```bash pnpm add declarative-hex-worlds ``` 2. **Fetch the FREE KayKit asset pack** (CC0-licensed; \~30 MB) ```bash pnpm exec declarative-hex-worlds bootstrap ``` Downloads 221 GLTF models into `public/assets/models/addons/kaykit_medieval_hexagon_pack/`. SHA-256 sidecar verified. 3. **Render your first harbor** ```tsx import { Canvas } from '@react-three/fiber'; import * as HexWorld from 'declarative-hex-worlds'; const plan = HexWorld.createGameboardBuilder({ seed: 'harbor-village-1', shape: { kind: 'rectangle', width: 6, height: 6 }, }).build(); const runtime = HexWorld.createGameboardRuntimeFromScenario({ plan, scenario: { actors: [], quests: [] }, }); export function Harbor() { return ( {/* your three.js scene of rt.snapshot() */} ); } ``` ## Explore the library [Section titled “Explore the library”](#explore-the-library) [Pieces & actors](/features/pieces-and-actors/)Declare buildings, units, decorations. The library spawns + binds them to ECS entities. [Multi-depth stacks](/features/multi-depth-stacks/)Stacked terrain (water → land → cliff → peak) with validated transitions. [Harbors](/features/harbors/)Sandy crescents, piers, fishing villages — composable harbor recipes. [Bridges & connectors](/features/bridges-and-connectors/)Spans, ferries, roads — connectivity validation across biome edges. [Quests](/features/quests/)Patrol routes, spawn groups, scripted scenarios. Deterministic replay. [Determinism & replay](/features/determinism-replay/)The full seed → blueprint → scenario → simulation chain, byte-for-byte. [Tile injection](/features/tile-injection/)Drop bespoke tiles into procedural maps without breaking constraints. [Prop injection](/features/prop-injection/)Authored set-pieces (a watchtower, a graveyard) layered onto generated terrain. [Cross-kit composition](/features/cross-kit-composition/)Mix the FREE pack with EXTRA-pack tiles or third-party GLTFs. [Movement & patrols](/features/movement-and-patrols/)Actor movement, patrol routes, navigation predicates. ## Why this exists [Section titled “Why this exists”](#why-this-exists) Hex-grid games are simple in concept and a maintenance nightmare in code. Tile adjacency rules, biome blending, asset selection, deterministic seeding, multiplayer-safe replay — each is a small problem; all of them together is a six-month detour from shipping a game. **declarative-hex-worlds** treats the whole stack as one declarative pipeline: recipe → blueprint → scenario → koota ECS world → React + Three.js render. You declare what you want and what’s allowed; the library decides what goes where, validates it, and gives you back a deterministic snapshot. The hex-tile assets are the [KayKit Medieval Hexagon Pack](https://kaykit.itch.io/medieval-hexagon-pack) by [Kay Lousberg](https://kaylousberg.com/) — gorgeous, low-poly, CC0. The library bootstraps them at install time; the npm tarball stays small (\~2.3 MB) and asset licensing stays clean. [Architecture](/about/architecture/)How recipe / blueprint / scenario / runtime fit together. [Design rationale](/about/design/)Why determinism is the product, and why react+three are runtime deps. [API reference](/reference/readme/)Every public symbol across the 21 subpath exports. [CLI reference](/guides/cli-reference/)36 commands — bootstrap, validate, simulate, coverage, and more. # Asset bootstrap > Materialize the KayKit Medieval Hexagon GLTF asset tree under your app's asset root. `declarative-hex-worlds` is an **asset-bootstrapping** library, not an asset-bundled one. The published npm tarball ships: * The typed runtime, CLI, React + Three.js bindings. * The FREE-edition `manifest.json` (metadata only — asset ids, paths, bounds, taxonomy, file sizes). The KayKit GLTF binaries themselves (≈30 MB for FREE, more for EXTRA) are fetched at install time by the CLI `bootstrap` subcommand, or by calling the programmatic [`bootstrapKayKitAssets`](/reference/bootstrap/) API directly. ## Why bootstrap, not bundle? [Section titled “Why bootstrap, not bundle?”](#why-bootstrap-not-bundle) * **Keeps the npm tarball lean** (\~1 MB shipped vs 30+ MB if bundled). * **Lets consumers re-use a single asset tree** across multiple apps via a shared `public/assets/models/` directory. * **Avoids redistributing CC0 binary blobs** through npm’s CDN. * **Supports the purchased EXTRA edition** without forcing every consumer to redistribute the larger archive. ## Quick start (FREE edition, from GitHub) [Section titled “Quick start (FREE edition, from GitHub)”](#quick-start-free-edition-from-github) ```bash pnpm add declarative-hex-worlds pnpm exec declarative-hex-worlds bootstrap ``` The default `--out` heuristic detects: 1. `public/assets/models/` (Vite / Next.js convention) — preferred. 2. `assets/models/` (fallback). 3. The current working directory. You can always pass `--out ` explicitly. After bootstrap, the tree lives at: ```text /addons/kaykit_medieval_hexagon_pack/ ├── Assets/gltf/ │ ├── buildings/{blue,green,red,yellow,neutral}/...gltf │ ├── decoration/{nature,props}/...gltf │ └── tiles/{base,coast,rivers,roads}/...gltf ├── Textures/ │ └── hexagons_medieval.png └── .bootstrap.json ``` ## Downloadable default packs [Section titled “Downloadable default packs”](#downloadable-default-packs) Beyond the FREE tile pack, three first-class CC0 packs by [Kay Lousberg](https://kaylousberg.com) compose into a full game from defaults — a hex board, playable characters, and enemies. Fetch any of them by id: `--out` names the raw-assets **root**; each pack lands in `//`, so the default-source resolvers below always find it: ```bash pnpm exec declarative-hex-worlds bootstrap --pack medieval-hexagon --out raw-assets pnpm exec declarative-hex-worlds bootstrap --pack adventurers --out raw-assets pnpm exec declarative-hex-worlds bootstrap --pack skeletons --out raw-assets # → raw-assets/{medieval-hexagon,adventurers,skeletons}/ ``` | Pack id | Role | Fills | | ------------------ | ------ | ------------------- | | `medieval-hexagon` | tiles | the terrain board | | `adventurers` | models | playable characters | | `skeletons` | models | enemies | Packs are **never** tracked in git — keep `raw-assets/` gitignored. They download from their upstream GitHub archives on demand. Programmatically: ```ts import { bootstrapPack, resolveDefaultPackKit, assertPackPresent, } from 'declarative-hex-worlds/bootstrap'; // Fetch a pack (lands in raw-assets/adventurers/). await bootstrapPack('adventurers', { rawAssetsRoot: 'raw-assets' }); // Compose defaults from whatever is downloaded. for (const pack of resolveDefaultPackKit('raw-assets')) { if (pack.present) console.log(`${pack.id} → ${pack.dir}`); } // Or require one, failing clearly (with the fetch command) if it is missing. const dir = assertPackPresent('skeletons', 'raw-assets'); ``` ## EXTRA edition (purchased) [Section titled “EXTRA edition (purchased)”](#extra-edition-purchased) The EXTRA edition is sold on [kaylousberg.itch.io](https://kaylousberg.itch.io) and ships extra `units/` GLTFs plus seasonal `hexagons_medieval_{Fall,Summer, Winter}.png` textures. Download the zip from itch.io, then: ```bash pnpm exec declarative-hex-worlds bootstrap \ --source zip \ --zip ~/Downloads/KayKit_Medieval_Hexagon_Pack_1.0_EXTRA.zip \ --edition extra \ --out public/assets/models ``` The CLI auto-detects the edition from the zip’s layout markers and refuses to overwrite a FREE-shaped target with EXTRA content (and vice-versa). ## Reproducible bootstraps [Section titled “Reproducible bootstraps”](#reproducible-bootstraps) The integrity sidecar (`.bootstrap.json`) records: * `schemaVersion` (currently `1.0.0`). * `edition` (`free` or `extra`). * `libraryVersion` (the library version that performed the bootstrap). * `sourceUrl` (the GitHub tarball URL, or `file://` for zip sources). * `fetchedAt` (ISO-8601 timestamp). * `files[]` — sorted list of `{path, sha256, bytes}` for every mirrored file. For deterministic CI builds, pass `fetchedAt` and `libraryVersion` to the programmatic API: ```ts import { bootstrapKayKitAssets } from 'declarative-hex-worlds/bootstrap'; await bootstrapKayKitAssets({ source: { kind: 'github', commit: 'main' }, // pin to a specific sha for reproducibility out: 'public/assets/models', fetchedAt: '2026-01-01T00:00:00.000Z', libraryVersion: '1.0.0', }); ``` ## Verifying an existing bootstrap [Section titled “Verifying an existing bootstrap”](#verifying-an-existing-bootstrap) ```bash pnpm exec declarative-hex-worlds bootstrap --verify --out public/assets/models ``` Re-hashes every file recorded in `.bootstrap.json` and reports any drift (missing files, size mismatches, hash mismatches). Exits non-zero on drift — useful as a CI step before deploys. ## Wiring the runtime to your bootstrap target [Section titled “Wiring the runtime to your bootstrap target”](#wiring-the-runtime-to-your-bootstrap-target) Three ways to tell the loaders where your bootstrap target lives: ```ts // 1. Per-call (most explicit) import { resolveManifestAssetUrl, freeManifest } from 'declarative-hex-worlds'; const url = resolveManifestAssetUrl(freeManifest.assets[0], { bootstrapAssetRoot: '/app/public/assets/models', }); // 2. Process-wide (app boot) import { setGameboardAssetRoot } from 'declarative-hex-worlds'; setGameboardAssetRoot('/app/public/assets/models'); // 3. Environment variable (Node consumers) // HEX_WORLDS_ASSET_ROOT=/app/public/assets/models ``` Resolution priority: explicit `setGameboardAssetRoot` override → `globalThis.HEX_WORLDS_ASSET_ROOT` → `process.env.HEX_WORLDS_ASSET_ROOT` → default `public/assets/models`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `bootstrap destination ... is not empty` [Section titled “bootstrap destination ... is not empty”](#bootstrap-destination--is-not-empty) The target already contains files. Pass `--force` to wipe and re-mirror, or pick a different `--out`. ### `zip contains the EXTRA edition but bootstrap was asked for FREE` [Section titled “zip contains the EXTRA edition but bootstrap was asked for FREE”](#zip-contains-the-extra-edition-but-bootstrap-was-asked-for-free) The zip’s layout markers identify it as EXTRA. Add `--edition extra` (and make sure you have a license / purchased copy). ### `failed to download KayKit FREE tarball ... unexpected status 404` [Section titled “failed to download KayKit FREE tarball ... unexpected status 404”](#failed-to-download-kaykit-free-tarball--unexpected-status-404) GitHub returned a non-2xx response. Common causes: * Pinned `--commit ` no longer exists (force-pushed or deleted). * Temporary GitHub outage. Retry, or pass `--source zip` with a locally downloaded copy. ### `bootstrap verify FAILED ... hash mismatch` [Section titled “bootstrap verify FAILED ... hash mismatch”](#bootstrap-verify-failed--hash-mismatch) A file on disk no longer matches its recorded sha256. Either a deploy modified the tree (unlikely — the tree should be read-only after bootstrap) or disk corruption. Re-run `bootstrap --force` to restore. ### Runtime loads 404 — wrong asset root [Section titled “Runtime loads 404 — wrong asset root”](#runtime-loads-404--wrong-asset-root) The runtime falls back to `public/assets/models` if no override is configured. If you bootstrapped to `assets/models` or elsewhere, set `HEX_WORLDS_ASSET_ROOT` or call `setGameboardAssetRoot(...)` at boot. ## Programmatic API [Section titled “Programmatic API”](#programmatic-api) See the [`bootstrap` reference](/reference/bootstrap/) for the full surface, including the {@link BootstrapKayKitAssetsOptions}, {@link BootstrapResult}, and {@link BootstrapSidecar} types. # Bindings and bundling > Subpath imports, trait identity hazard, the first-class React + Three.js binding model. ## Subpath imports [Section titled “Subpath imports”](#subpath-imports) The library publishes \~40 subpaths under `package.json#exports`. Pick the one you need: ```ts // Specific subpath — only that domain's symbols + its transitive deps land in your bundle import { createGameboardBuilder } from 'declarative-hex-worlds/gameboard'; import { GameboardProvider, useGameboardRuntime } from 'declarative-hex-worlds/react'; import { loadGameboardPlacementObject } from 'declarative-hex-worlds/three'; // Umbrella — everything (use only for prototyping) import * as lib from 'declarative-hex-worlds'; ``` The umbrella re-exports everything from every sub-package; consumers prototyping in a sandbox can use it. For production code, prefer subpath imports — they let bundlers tree-shake aggressively because `sideEffects: false` is set on the package. ## React + Three.js are direct dependencies, not peer-deps [Section titled “React + Three.js are direct dependencies, not peer-deps”](#react--threejs-are-direct-dependencies-not-peer-deps) PRD invariant §5: `react`, `react-dom`, `three`, `koota`, `honeycomb-grid`, `seedrandom` are direct `dependencies`. The library is unusable without them; bundling them defensively is not a consumer choice. Why this matters: 1. **No version skew between library code and consumer code.** A common peer-dep failure mode is the library being tested against React 18 while the consumer ships React 19; subtle hook behavior changes. With direct deps, the library pins what it tests against. 2. **No “did you remember to install React?” footgun.** `npm install declarative-hex-worlds` is enough. 3. **The library can ship binding implementations confidently.** `useGameboardRuntime()` knows it’s using the same React the library tested against. If your app uses a different React version than the library, npm’s de-duplication generally resolves to your version (your `package.json` wins). For Three.js the same de-dup applies. The library tests against the latest patches of each major; check `package.json` for the current pin. ## Trait identity hazard (and why `splitting: true` matters) [Section titled “Trait identity hazard (and why splitting: true matters)”](#trait-identity-hazard-and-why-splitting-true-matters) The library’s koota traits (in `src/traits/`) are unique objects compared by reference, not name. If two chunks both define the same trait, koota treats them as different traits and your entities don’t match queries. The library protects you by: 1. **Single `traits/` barrel.** Every trait declaration lives at `src/traits/.ts` and is re-exported from `src/traits/index.ts`. There’s only one declaration per trait. 2. **`splitting: true` in tsup.** Each subpath becomes its own chunk, but shared dependencies (including trait declarations) land in shared chunks. The trait identity test (Epic E4) imports the same trait from `/koota` and from the umbrella and asserts reference-equality. For consumers: import traits through whichever subpath is convenient. They’re the same object regardless of import path. The E4 test pins this; if it ever fails, your bundle has a chunk-splitting bug. ## When you need to extend with custom traits [Section titled “When you need to extend with custom traits”](#when-you-need-to-extend-with-custom-traits) ```ts import { trait } from 'koota'; import { type GameboardActor } from 'declarative-hex-worlds/actors'; // Define a custom trait const Inventory = trait<{ items: string[] }>(); // Spawn an entity with both library traits and your custom trait const myActor = world.spawn(GameboardActor({ id: 'player', team: 'players' }), Inventory({ items: [] })); ``` Your custom traits should live in your own module, exported once. If you split them across files via re-exports, ensure `splitting: true` in your bundler too (most modern bundlers do this by default). ## SSR / server-side rendering [Section titled “SSR / server-side rendering”](#ssr--server-side-rendering) The `react` subpath uses hooks that need the React provider. The `three` subpath has DOM dependencies (canvas, WebGL) and shouldn’t import in SSR. The pattern: ```ts // pages/board.tsx (Next.js) import dynamic from 'next/dynamic'; const Board = dynamic(() => import('./Board'), { ssr: false }); ``` The library doesn’t ship an SSR-safe variant; render boards client-only. ## Bundle size [Section titled “Bundle size”](#bundle-size) The umbrella is \~250 KB minified (most of that is the FREE manifest literal — 16k LOC of asset metadata). Subpath imports cut to whatever you actually use; `/coordinates` alone is \~8 KB. The bundled FREE manifest is the product (PRD §B2 rejection): it’s why “install + bootstrap + render” is a 30-line quickstart. If you don’t want it in your bundle, import `/coordinates`, `/gameboard`, etc. directly and load the manifest yourself via `loadFreeManifest()` (B2b). ## Subpath table [Section titled “Subpath table”](#subpath-table) See the [API reference](/reference/) for the full list. Every subpath has its own page documenting what it exports. # CLI Reference > Every command, flag, and option accepted by the declarative-hex-worlds CLI binary. import { Aside } from ‘@astrojs/starlight/components’; This page is autogenerated from \`src/cli/cli.ts\` by \`scripts/generate-cli-reference.ts\`. Edits land in the source, not here. The library ships a Node CLI binary named `declarative-hex-worlds`. Invoke via `pnpm exec declarative-hex-worlds ` or `npx declarative-hex-worlds `. ## Bootstrap first [Section titled “Bootstrap first”](#bootstrap-first) Most commands operate against a local FREE or EXTRA pack tree. Before any tile / asset command, run: ```bash pnpm exec declarative-hex-worlds bootstrap ``` This downloads the KayKit FREE pack and mirrors it flat under `./models/` (default). See the [bootstrap guide](/guides/bootstrap/) for source modes, integrity verification, and customizing the output directory. ## Full usage [Section titled “Full usage”](#full-usage) ```plaintext declarative-hex-worlds [options] Commands: doctor Report local source and docs status validate Validate local FREE or EXTRA source counts manifest Generate a manifest JSON from a source folder validate-manifest Validate a generated manifest JSON and optionally write a normalized copy analyze Analyze tile bounds, grid scale, row spacing, and warnings declarations Emit tile declarations from a source folder, manifest, or registry guide-permutations Emit guide-labeled road, river, crossing, and coast permutation metadata guide-scenarios Emit extracted guide-page scenario metadata and validate page assets guide-usages Emit renderer-ready page-level guide asset occurrence metadata guide-render-requests Emit URL-resolved guide render request queues and optional page groups guide-apis Emit public API to guide-page and asset coverage metadata guide-assets Emit asset id to guide-page, API, docs, and visual coverage metadata guide-roles Emit public role to guide-page, asset, and API coverage metadata coverage Emit release-readiness coverage JSON or Markdown blueprint Compile high-level 2.5D board intent to a recipe, plan, scenario, and diagnostics summarize-plan Summarize terrain, placement, feature, asset, and local-only usage in a plan, recipe, scenario, or blueprint summarize-scenario Summarize board, actor, spawn, patrol, quest, and local-only usage in a scenario pieces Validate piece declarations and optionally emit seeded piece fill rules place-piece Inspect and append one declared piece against a saved GameboardPlan, recipe, or scenario validate-plan Validate a GameboardPlan JSON with optional registry rules analyze-layout Analyze seeded layout fill rules against a saved GameboardPlan, recipe, or scenario spawn-groups Plan separated spawn groups and route diagnostics against a plan, recipe, or scenario patrol-routes Plan NPC/enemy patrol waypoints and segment diagnostics against a plan, recipe, or scenario patrol-script Create executable simulation command steps from planned patrol routes and actor assignments validate-recipe Validate a GameboardRecipe JSON and optionally compile it to a plan validate-scenario Validate a GameboardScenario JSON and optionally compile its plan validate-simulation Validate a GameboardScenario simulation script without executing it snapshot Emit a neutral ECS interop snapshot from a plan, recipe, or scenario simulate-scenario Run a GameboardScenario simulation script and emit event records, final plan, or ECS interop compatibility Analyze one external GLB/GLTF for hex-tile compatibility and placement suggestions piece Emit a custom piece declaration from an external GLB/GLTF compatibility scan pieces-from-assets Scan GLB/GLTF files and emit custom piece declarations plus compatibility summaries bind Scan an assets directory and emit a Zod-validated AssetSourceSpec JSON (the agent path — no prompts) init Interactively bind an assets directory to an AssetSourceSpec (the human path — a TTY wizard) web Bind an assets directory via a local web form (the visual path — a loopback config UI) extract Copy GLTF assets and write a manifest to an output folder (alias: ingest) bootstrap Materialize KayKit GLTF assets under a consumer asset root (PRD RB) Run `declarative-hex-worlds --help` for that command's full flag reference. Global options (accepted by every command): --source Override the source root every command resolves against (default: ./references/KayKit_Medieval_Hexagon_Pack_1.0_, chosen by --edition). --edition free|extra Pack edition used to pick the default --source root and validate assets (default: free). Options: --actorId --allowExpectationFailures --allowInvalid --allowUnknownAssetIds --allowUnknownAssets --asset --asset-root --assetBaseUrl --assetEdition free|extra|all --assetId --assetId --assetIdPrefix --assetIds --assetIds --assets --assetScope free|extra|all --assignments --blueprint --boardForwardEdge 0..5 --categories --category tiles|buildings|decoration|units --checksPassed --cols --rows --commit --config --count --coverage --creator --defaultTerrain --dir --edition free|extra --editionScope free|extra|mixed|reference --emitRules --emitSourceUrls --excludeActors --excludePlacements --excludeQuests --excludeSpawnGroups --excludeTags --excludeTimeline --faction --failOnBlockedQuest --failOnWarning --fill --force --format json --format markdown|json --freeOnly --generatedAt --grouped --groups --groups --guideRole --harbors --height --id --idPrefix --ids --include-source-formats --includeAbsolutePaths --includeGroups --includeInterop --includePlan --includeRecipe --includeReport --includeReports --includeScenario --includeScenarioInspection --includeTreatments --intendedRole tile|prop|structure|unit --json --license --manifest --markdown --maxCount --maxElevation --minCount --minimumEdition free|extra|all --mode per-piece|pool --modelForward +z|-z|+x|-x --name --out --outInterop --outJson --outManifest --outMarkdown --outPlan --outRecipe --outScenario --outScenarioInspection --overrides --pack --page --pieceId --pieceIdPrefix --pieceOverrides --pieces --pieceSourceRoot --pieceSourceRoots --plan --port --publicApi --publicApi --radius --recipe --registry --requiresExtra --role --role --role surface|building|unit|prop|tree|scatter|landmark|custom --roles --rounds --routeId --routes --ruleIdPrefix --rules --scenario --scenario --scenarioId --script --seed --shape rectangle|hexagon --source github|zip --sourcePack --sources --spawnCount --spawnEdgePadding --spawnMinDistance --spawnSeed --tags --textureSet --topAssetLimit --topAssets --towns --unencodedSourceUrls --verify --waterFill --width --width, --height, --radius, --seed, --faction, --textureSet, --defaultTerrain, --waterFill, --maxElevation, --towns, --harbors, --shape --zip ``` ## Common command recipes [Section titled “Common command recipes”](#common-command-recipes) ### Doctor [Section titled “Doctor”](#doctor) Report local source + docs status. Useful first-time setup sanity check: ```bash declarative-hex-worlds doctor ``` ### Validate a FREE pack [Section titled “Validate a FREE pack”](#validate-a-free-pack) ```bash declarative-hex-worlds validate --edition free --source ./references/KayKit_Medieval_Hexagon_Pack_1.0_FREE ``` ### Compile a blueprint to a plan + scenario [Section titled “Compile a blueprint to a plan + scenario”](#compile-a-blueprint-to-a-plan--scenario) ```bash declarative-hex-worlds blueprint \ --blueprint ./blueprint.json \ --outScenario ./scenario.json \ --includeScenario ``` ### Run a simulation script [Section titled “Run a simulation script”](#run-a-simulation-script) ```bash declarative-hex-worlds simulate-scenario \ --scenario ./scenario.json \ --script ./simulation.script.json \ --rounds 10 ``` ### Emit coverage ledger [Section titled “Emit coverage ledger”](#emit-coverage-ledger) ```bash declarative-hex-worlds coverage \ --checksPassed \ --outJson docs/release-readiness.json \ --outMarkdown docs/release-readiness.md ``` ## Safe output paths [Section titled “Safe output paths”](#safe-output-paths) Every `--out*` flag goes through `safeResolveOutput` ([PRD C1](https://github.com/jbcom/declarative-hex-worlds/blob/main/docs/PRD/1.0.md#c1)). Paths that escape the current working directory throw before any write happens. `extract` refuses to wipe a non-empty destination unless `--force` is passed. Setting \`HEX\_WORLDS\_OUT\_ROOT\` to an absolute path widens the output jail beyond the working directory. Use only in controlled CI environments. Never set it to \`/\` or any ancestor of sensitive paths — the CLI will write there without further restrictions. ## JSON flag schemas and error contracts [Section titled “JSON flag schemas and error contracts”](#json-flag-schemas-and-error-contracts) Several flags accept JSON values directly or via a file path: * **`--pieceSourceRoots`** — `{ "sourceRoots": { "": "" } }`. Keys must match `[A-Za-z0-9_:-]+`; any key that is a prototype-pollution vector (`__proto__`, `constructor`, `prototype`) is rejected before the object is parsed. * **`--pieceOverrides`** — `{ "overrides": { "": { "footprint"?: ..., "criteria"?: ..., "tags"?: string[], "metadata"?: ... } } }`. * **`--pieces`** (all `pieces*` commands) — a path to a JSON file produced by `pieces-from-assets`. Not accepted as inline JSON. All JSON parsing errors surface as `GameboardCliError` with a descriptive message on stderr. Exit code is always non-zero on error. ## Errors [Section titled “Errors”](#errors) CLI errors are `GameboardCliError` instances ([PRD D2](https://github.com/jbcom/declarative-hex-worlds/blob/main/docs/PRD/1.0.md#d2)). The library’s error taxonomy is documented in the [errors reference](/reference/errors/). # Determinism Contract > Seed model, replay guarantees, where Date / Math.random are (and aren't) permitted. The library is deterministic: identical inputs (seed + scenario + script) produce **byte-identical outputs** across processes and platforms. This is PRD invariant §1, and the test suite has a cross-process gate (Epic E1) that spawns N subprocesses and asserts byte-identity to keep it honest. ## What “deterministic” means here [Section titled “What “deterministic” means here”](#what-deterministic-means-here) Given the same: * **Seed** (`"my-seed"` or any string passed to `createSeededGameboardPlan(...)`). * **Scenario** (the compiled `GameboardScenario` JSON). * **Simulation script** (the array of timed commands). …you get the same: * Generated plan: same tiles in the same order, same placements with the same attributes. * Simulation event records: same event types, same ordering, same timestamps. * Runtime snapshots: same actor positions, quest progress, patrol waypoints. …across Node 22 and Node 24, across macOS / Linux / Windows, across `pnpm test` runs separated by months. ## The seed model [Section titled “The seed model”](#the-seed-model) The library uses [seedrandom](https://github.com/davidbau/seedrandom) under `src/scenario/recipe.ts` and `src/coordinates/layout.ts`. Every random decision threads through a `seedrandom.PRNG` instance derived from the seed: ```ts import { createSeededGameboardPlan } from 'declarative-hex-worlds'; const plan = createSeededGameboardPlan({ seed: 'harbor-village-7', shape: { kind: 'rectangle', width: 8, height: 6 }, }); // `plan.tiles`, `plan.placements`, `plan.warnings` — all reproducible from "harbor-village-7" ``` Pass the same seed twice, get the same plan twice. Pass a different seed, get a different plan. ## Where `Date.now`, `Math.random`, `performance.now` are forbidden [Section titled “Where Date.now, Math.random, performance.now are forbidden”](#where-datenow-mathrandom-performancenow-are-forbidden) PRD invariant §2: **never** in `src/`. Their use would inject system entropy into a “pure” runtime function and break the determinism contract. The lint config (`biome.json`) has `noGlobalIsNan`-style rules to flag them. The single exception is `src/cli/cli.ts:2296` where a `generatedAt` timestamp gets a default from `Date.now()` — and even there it’s overridable via `--generatedAt`. ## What about RNG inside three.js / koota? [Section titled “What about RNG inside three.js / koota?”](#what-about-rng-inside-threejs--koota) Neither uses non-determinism in their hot paths. three’s geometry math is pure; koota’s entity allocation is monotonic. The library doesn’t expose three’s `Math.random()`-using helpers (e.g. `THREE.MathUtils.randFloat`) — consumers who want random placement decisions thread through the library’s seeded helpers instead. ## Replay guarantees [Section titled “Replay guarantees”](#replay-guarantees) For a saved scenario, you can: 1. **Re-derive the plan** from `scenario.seed + scenario.shape`. 2. **Re-execute the simulation** from `scenario.scriptId + scenario.script`. 3. **Snapshot the world** at any tick and serialize via `runtime.snapshot()` → matches across runs. This is what enables: * Server-authoritative simulation (server runs the script; clients verify by replay). * Save games (just store the seed + script, not the world state). * Cross-process testing (the E1 gate compares JSON snapshots across spawned subprocesses). ## What breaks determinism (and what doesn’t) [Section titled “What breaks determinism (and what doesn’t)”](#what-breaks-determinism-and-what-doesnt) | Breaks ❌ | Safe ✅ | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Mutating `freeManifest` at runtime | Reading `freeManifest` | | Calling `Date.now()` inside a custom action | Passing a fixed `generatedAt` ISO string to CLI commands | | Iterating `Map` / `Set` insertion-order across processes with different V8 versions | The library uses `[...].sort()` everywhere insertion-order matters | | Concurrent `async` decisions in actions | Synchronous action handlers (the default) | | Using `JSON.stringify(...)` on objects with circular refs | `structuredClone(plan)` (B8 replaced the JSON dance) | ## Testing your own determinism [Section titled “Testing your own determinism”](#testing-your-own-determinism) ```ts import { createSeededGameboardPlan } from 'declarative-hex-worlds'; import { expect, test } from 'vitest'; test('same seed → same plan', () => { const a = createSeededGameboardPlan({ seed: 's', shape: { kind: 'rectangle', width: 4, height: 4 } }); const b = createSeededGameboardPlan({ seed: 's', shape: { kind: 'rectangle', width: 4, height: 4 } }); expect(JSON.stringify(a)).toBe(JSON.stringify(b)); }); ``` For cross-process determinism, mirror what the E1 gate does: spawn `node --eval` subprocesses with the same script, capture stdout, compare. ## Future [Section titled “Future”](#future) PRD Epic E1 ratchets this from a contract to a permanent test gate. Once E1 lands, every PR runs the cross-process determinism check. # Error taxonomy > GameboardError + 6 subclasses for structured catch-by-instanceof handling. import { Aside } from ‘@astrojs/starlight/components’; The library throws structured errors. Every library-originated throw is a `GameboardError` subclass — consumers can branch on `instanceof` without regex’ing messages. Implementation: \`src/errors/index.ts\`. 152 throw sites migrated (PRD D2). Messages are stable; consumers that match on text today keep working. ## The hierarchy [Section titled “The hierarchy”](#the-hierarchy) ```ts import { GameboardError, // base — catch this to handle ANY library error GameboardValidationError, // input data failed structural validation GameboardManifestError, // manifest is malformed / version-incompatible GameboardScenarioError, // scenario JSON / blueprint / recipe failed to compile or load GameboardRuntimeError, // runtime hit unrecoverable state GameboardCliError, // CLI got invalid flags / illegal output paths GameboardIoError, // ingest / bootstrap / filesystem couldn't proceed } from 'declarative-hex-worlds/errors'; ``` Or import from the umbrella: ```ts import { GameboardValidationError } from 'declarative-hex-worlds'; ``` ## Domain → subclass mapping [Section titled “Domain → subclass mapping”](#domain--subclass-mapping) | Source domain | Subclass | Examples | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------- | | `src/rules/**` (plan + scenario validation) | `GameboardValidationError` | rule conflict, blocked tile, invalid placement | | `src/manifest/**` shape errors | `GameboardManifestError` | unknown asset category, invalid export identifier | | `src/ingest/**` filesystem | `GameboardIoError` | missing GLTF source directory | | `src/cli/commands/bootstrap/**` | `GameboardIoError` | archive-zip download failure, zip extract failure | | `src/scenario/**` (recipe / blueprint / catalog) | `GameboardScenarioError` | scenario did not compile, recipe missing tiles | | `src/gameboard/**`, `src/coordinates/**`, `src/simulation/**`, `src/koota/**`, `src/systems/**`, `src/movement/**`, `src/patrol/**`, `src/quests/**`, `src/actors/**`, `src/pieces/**`, `src/interop/**`, `src/commands/**`, `src/selectors/**`, `src/three/**`, `src/react/**` | `GameboardRuntimeError` | unknown entity, broken trait shape, missing tile at coordinates | | `src/cli/cli.ts` | `GameboardCliError` | missing required flag, illegal path | ## Usage patterns [Section titled “Usage patterns”](#usage-patterns) **Catch a specific category**: ```ts import { GameboardValidationError, validateGameboardScenario } from 'declarative-hex-worlds'; try { const scenario = validateGameboardScenario(input); } catch (e) { if (e instanceof GameboardValidationError) { console.error('Scenario invalid:', e.message); showUserError(e.message); return; } throw e; // re-throw anything unexpected } ``` **Catch any library error** (separate from genuine bugs like `TypeError`, `ReferenceError`): ```ts import { GameboardError } from 'declarative-hex-worlds'; try { await loadAndRun(input); } catch (e) { if (e instanceof GameboardError) { // library-originated — show a UI error showError(`Library error: ${e.message}`); return; } // genuine bug — let it propagate to the global handler throw e; } ``` **Walk the cause chain** (when `cause` is set): ```ts try { await bootstrapKayKitAssets(options); } catch (e) { if (e instanceof GameboardError && e.cause instanceof Error) { console.error('Cause:', e.cause.message); } throw e; } ``` ## Constructor shape [Section titled “Constructor shape”](#constructor-shape) Every subclass takes `(message, options?)`: ```ts class GameboardError extends Error { constructor(message: string, options?: { cause?: unknown }); } ``` Subclasses set `.name` to their class name automatically (`new.target.name`), so error logs read `GameboardValidationError: ...` not `Error: ...`. ## Stable messages [Section titled “Stable messages”](#stable-messages) PRD D2 preserved every existing error message verbatim during the migration. Consumers that grep messages today keep working. Don’t change a message lightly; treat it as part of the API contract. ## What’s NOT a `GameboardError` [Section titled “What’s NOT a GameboardError”](#whats-not-a-gameboarderror) * `TypeError` / `ReferenceError` / `RangeError` — these are bugs in the library or in your usage. Report them. * Errors thrown from npm dependencies (`tar`, `seedrandom`, etc.) — these propagate unwrapped. * Browser-side errors from `three.js` / WebGL — these are rendering issues, not library logic errors. # Get started > Install, bootstrap the FREE asset pack, and render your first hexagon gameboard. `declarative-hex-worlds` is a deterministic, ECS-driven, React + Three.js library for building hex-grid games on top of the KayKit Medieval Hexagon GLTF pack. This page gets you from `pnpm add` to rendered hexes in under five minutes. ## 1. Install + bootstrap [Section titled “1. Install + bootstrap”](#1-install--bootstrap) ```bash pnpm add declarative-hex-worlds pnpm exec declarative-hex-worlds bootstrap ``` The library ships the typed runtime + the FREE manifest metadata; the GLTF binaries themselves are downloaded at install time from the [KayKit upstream GitHub repo](https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0) by the `bootstrap` subcommand. The default `--out` heuristic targets `public/assets/models/` (Vite / Next.js convention); see [Asset bootstrap](/guides/asset-bootstrap/) for the full workflow including the EXTRA edition and reproducible-build options. After bootstrap, your asset tree looks like: ```text public/assets/models/addons/kaykit_medieval_hexagon_pack/ ├── Assets/gltf/... # 221 .gltf + .bin files ├── Textures/... └── .bootstrap.json # integrity sidecar ``` ## 2. Point the runtime at your bootstrap target [Section titled “2. Point the runtime at your bootstrap target”](#2-point-the-runtime-at-your-bootstrap-target) ```ts import { setGameboardAssetRoot } from 'declarative-hex-worlds'; // Call once at app boot: setGameboardAssetRoot('/public/assets/models'); ``` Or set `HEX_WORLDS_ASSET_ROOT` in your environment — the runtime falls through `override → globalThis → process.env → 'public/assets/models'` default. ## 3. Build a deterministic gameboard [Section titled “3. Build a deterministic gameboard”](#3-build-a-deterministic-gameboard) ```ts import { buildGameboardBlueprint, createGameboardWorldFromScenario, } from 'declarative-hex-worlds'; // One-call: shape + seed → fully populated scenario. const { scenario } = buildGameboardBlueprint({ shape: { kind: 'rectangle', width: 12, height: 8 }, seed: 'tutorial-island', }); // Spawn into a Koota world with traits, occupancy, navigation graphs, etc. const runtime = createGameboardWorldFromScenario(scenario); ``` `buildGameboardBlueprint` compiles biome fills, mountain ranges, towns, roads, rivers, harbors, and prop clusters into a `GameboardScenario` that the Koota runtime can spawn deterministically from a seed. ## 4. Render with the React + Three.js bindings [Section titled “4. Render with the React + Three.js bindings”](#4-render-with-the-react--threejs-bindings) ```tsx import { Canvas } from '@react-three/fiber'; import { GameboardScene } from 'declarative-hex-worlds/react'; export function App({ runtime }: { runtime: GameboardRuntime }) { return ( ); } ``` React + Three.js are first-class runtime dependencies (not optional peers), so the bindings work without any extra installs. ## 5. Next steps [Section titled “5. Next steps”](#5-next-steps) * [Asset bootstrap](/guides/asset-bootstrap/) — full bootstrap workflow, including EXTRA edition, reproducible builds, and `--verify`. * [KayKit upstream layout](/guides/kaykit-upstream-layout/) — the on-disk shape the bootstrap step mirrors. * [CLI reference](/guides/cli-reference/) — every CLI subcommand and flag. * [`declarative-hex-worlds`](/reference/) — typed API reference. # Guide scenario coverage > Per-page KayKit user-guide coverage matrix. The KayKit user guide is decomposed into 19 source-page scenarios. This page is the human-facing map for those scenarios; the machine-readable source remains `listKayKitGuideScenarios()`, `describeKayKitGuideScenarioCoverage()`, `listKayKitGuideScenarioAssetUsages()`, `listKayKitGuideScenarioAssetRenderRequests()`, `listKayKitGuideScenarioAssetRenderGroups()`, `listKayKitGuideAssetCoverages()`, `listKayKitGuideRoleCoverages()`, `listKayKitGuidePublicApiCoverages()`, and the `guide-scenarios` / `guide-usages` / `guide-render-requests` / `guide-assets` / `guide-roles` / `guide-apis` CLI commands. Use this page when deciding whether a guide image has public API treatment, docs, and visual review coverage. Use the catalog API or CLI when a tool needs exact asset ids or public treatment records. ```sh node dist/cli.js guide-scenarios --markdown > docs/guides/guide-scenario-coverage.md node dist/cli.js guide-scenarios --page 15 --includeTreatments --json node dist/cli.js guide-usages --page 16,17,18 --json node dist/cli.js guide-render-requests --page 16,17,18 --assetBaseUrl /assets/extra --includeGroups --out /tmp/kaykit-guide-render-requests.json node dist/cli.js guide-assets --assetId hex_road_M --json node dist/cli.js guide-roles --role prop --json node dist/cli.js guide-apis --publicApi GameboardBuilder.addHarbor --json ``` ## Coverage Contract [Section titled “Coverage Contract”](#coverage-contract) * Every extracted guide page has exactly one `page-NN-*` scenario. * Every scenario lists its source PNG, edition scope, public API surfaces, docs, and visual artifacts. * Every FREE and local EXTRA asset id appears in at least one scenario unless it is a reference-only license/supporter page with no assets. * Every asset-bearing scenario can be expanded into public treatment records with `listKayKitGuideScenarioTreatments(id)`. * Every page-level asset occurrence can be expanded into renderer-ready usage records with `listKayKitGuideScenarioAssetUsages()`. * Every FREE and local EXTRA asset id can be inverted back to pages/APIs/docs and screenshots with `listKayKitGuideAssetCoverages()`. * Every public treatment role can be inverted back to pages/assets/APIs with `listKayKitGuideRoleCoverages()`. * Every public API string in a scenario can be inverted back to pages/assets with `listKayKitGuidePublicApiCoverages()`. ## Page Matrix [Section titled “Page Matrix”](#page-matrix) ### Page 01 - Overview And License Contract [Section titled “Page 01 - Overview And License Contract”](#page-01---overview-and-license-contract) * Scenario: `page-01-overview-and-license` * Edition: `reference` * Source image: `docs/assets/kaykit-guide/pages/page-01.png` * Asset coverage: 0 unique, 0 FREE, 0 EXTRA, 0 occurrences * Roles: reference-only * Public API treatment: `freeManifest`, `listKayKitAssetPublicTreatments`, `listKayKitGuideScenarios` * Visual artifacts: `docs/assets/kaykit-guide/montage.png`, `docs/assets/kaykit-guide/pages/page-01.png` * Docs: `README.md`, `NOTICE.md`, `docs/pillars/00-library-charter.md` ### Page 02 - Buildings, Props, And Factions [Section titled “Page 02 - Buildings, Props, And Factions”](#page-02---buildings-props-and-factions) * Scenario: `page-02-buildings-props-and-factions` * Edition: `mixed` * Source image: `docs/assets/kaykit-guide/pages/page-02.png` * Asset coverage: 164 unique, 119 FREE, 45 EXTRA, 164 occurrences * Roles: `faction-building`, `neutral-structure`, `prop` * Public API treatment: `GameboardBuilder.addBridge`, `GameboardBuilder.addConstructionSite`, `GameboardBuilder.addFactionBuilding`, `GameboardBuilder.addFlag`, `GameboardBuilder.addFortification`, `GameboardBuilder.addHarbor`, `GameboardBuilder.addNeutralStructure`, `GameboardBuilder.addProp`, `GameboardBuilder.addPropCluster`, `GameboardBuilder.addSettlement`, `GameboardBuilder.addSiegeProjectile`, `createGameboardLayoutFillRuleFromPiece`, `createGameboardPlanFromRecipe`, `factionBuildingAssetId`, `flagAssetId`, `listPropClusterAssets` * Visual artifacts: `tests/browser/__screenshots__/free-guide-page-nature-stacks-buildings-props.png`, `tests/browser/__screenshots__/extra-local-all-buildings-factions-neutral-harbors.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/guides/public-api.md` ### Page 03 - Road Variations [Section titled “Page 03 - Road Variations”](#page-03---road-variations) * Scenario: `page-03-road-variations` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-03.png` * Asset coverage: 15 unique, 15 FREE, 0 EXTRA, 15 occurrences * Roles: `road-tile` * Public API treatment: `GameboardBuilder.addRoadPath`, `listRoadGuidePermutations`, `selectRoadVariant`, `selectRoadVariantByLabel` * Visual artifacts: `tests/browser/__screenshots__/free-guide-roads-all-labels-rotations.png` * Docs: `docs/pillars/01-tiles-connectivity.md`, `docs/pillars/04-visual-verification.md` ### Page 04 - River Variations [Section titled “Page 04 - River Variations”](#page-04---river-variations) * Scenario: `page-04-river-variations` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-04.png` * Asset coverage: 30 unique, 30 FREE, 0 EXTRA, 30 occurrences * Roles: `river-tile` * Public API treatment: `GameboardBuilder.addRiverPath`, `listRiverCrossingGuidePermutations`, `listRiverCurvyGuidePermutations`, `listRiverGuidePermutations`, `selectRiverCrossingVariant`, `selectRiverVariant`, `selectRiverVariantByLabel` * Visual artifacts: `tests/browser/__screenshots__/free-guide-rivers-all-labels-rotations-water-waterless.png`, `tests/browser/__screenshots__/free-guide-river-curvy-crossings-all-modes.png` * Docs: `docs/pillars/01-tiles-connectivity.md`, `docs/pillars/04-visual-verification.md` ### Page 05 - Nature And Decoration Contents [Section titled “Page 05 - Nature And Decoration Contents”](#page-05---nature-and-decoration-contents) * Scenario: `page-05-nature-contents` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-05.png` * Asset coverage: 77 unique, 68 FREE, 9 EXTRA, 77 occurrences * Roles: `nature-decoration`, `prop` * Public API treatment: `GameboardBuilder.addFlag`, `GameboardBuilder.addForest`, `GameboardBuilder.addHarbor`, `GameboardBuilder.addHill`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.addNature`, `GameboardBuilder.addProp`, `GameboardBuilder.addPropCluster`, `GameboardBuilder.scatterDecorations`, `createGameboardBlueprintRecipe`, `createGameboardLayoutFillRuleFromPiece`, `flagAssetId`, `listPropClusterAssets` * Visual artifacts: `tests/browser/__screenshots__/free-guide-page-nature-stacks-buildings-props.png`, `tests/browser/__screenshots__/extra-local-all-decoration-nature-props.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/pillars/05-koota-runtime-rules.md` ### Page 06 - Nature Usage Guide [Section titled “Page 06 - Nature Usage Guide”](#page-06---nature-usage-guide) * Scenario: `page-06-nature-usage` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-06.png` * Asset coverage: 42 unique, 42 FREE, 0 EXTRA, 42 occurrences * Roles: `nature-decoration` * Public API treatment: `GameboardBuilder.addForest`, `GameboardBuilder.addHill`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.addNature`, `GameboardBuilder.scatterDecorations`, `createGameboardBlueprintPlan`, `createGameboardLayoutArchetypeRegistry`, `createGameboardLayoutFillRuleFromPiece`, `inspectGameboardBlueprint` * Visual artifacts: `tests/browser/__screenshots__/free-guide-page-nature-stacks-buildings-props.png`, `tests/browser/__screenshots__/free-generated-piece-recipe.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/pillars/05-koota-runtime-rules.md` ### Page 07 - Water Usage Guide [Section titled “Page 07 - Water Usage Guide”](#page-07---water-usage-guide) * Scenario: `page-07-water-usage` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-07.png` * Asset coverage: 44 unique, 44 FREE, 0 EXTRA, 44 occurrences * Roles: `base-tile`, `coast-tile`, `neutral-structure`, `river-tile` * Public API treatment: `GameboardBuilder.addBridge`, `GameboardBuilder.addHarbor`, `GameboardBuilder.addNeutralStructure`, `GameboardBuilder.addRiverPath`, `GameboardBuilder.setCoastEdges`, `GameboardBuilder.setTerrain`, `GameboardBuilder.setTileAsset`, `createGameboardPlanFromRecipe`, `createGameboardPlanFromTiles`, `listCoastGuidePermutations`, `listRiverCrossingGuidePermutations`, `listRiverCurvyGuidePermutations`, `listRiverGuidePermutations`, `selectCoastVariant`, `selectCoastVariantByLabel`, `selectRiverCrossingVariant`, `selectRiverVariant`, `selectRiverVariantByLabel` * Visual artifacts: `tests/browser/__screenshots__/free-guide-coasts-all-labels-rotations-water-waterless.png`, `tests/browser/__screenshots__/free-guide-rivers-all-labels-rotations-water-waterless.png`, `tests/browser/__screenshots__/extra-harbor-gameboard.png` * Docs: `docs/pillars/01-tiles-connectivity.md`, `docs/pillars/04-visual-verification.md` ### Page 08 - Taller Hex Tiles [Section titled “Page 08 - Taller Hex Tiles”](#page-08---taller-hex-tiles) * Scenario: `page-08-taller-hex-tiles` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-08.png` * Asset coverage: 3 unique, 3 FREE, 0 EXTRA, 3 occurrences * Roles: `base-tile`, `support-tile` * Public API treatment: `GameboardBuilder.addElevationRamp`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.setElevation`, `GameboardBuilder.setTileAsset`, `createGameboardBlueprintRecipe`, `createGameboardPlanFromRecipe` * Visual artifacts: `tests/browser/__screenshots__/free-guide-page-nature-stacks-buildings-props.png`, `tests/browser/__screenshots__/free-gameboard-recipe.png`, `tests/browser/__screenshots__/free-blueprint-builder-showcase.png` * Docs: `docs/pillars/01-tiles-connectivity.md`, `docs/pillars/05-koota-runtime-rules.md` ### Page 09 - World Design Example [Section titled “Page 09 - World Design Example”](#page-09---world-design-example) * Scenario: `page-09-world-design-example` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-09.png` * Asset coverage: 61 unique, 61 FREE, 0 EXTRA, 61 occurrences * Roles: `base-tile`, `nature-decoration`, `neutral-structure`, `road-tile` * Public API treatment: `GameboardBuilder.addBridge`, `GameboardBuilder.addForest`, `GameboardBuilder.addHill`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.addNature`, `GameboardBuilder.addNeutralStructure`, `GameboardBuilder.addRoadPath`, `GameboardBuilder.scatterDecorations`, `GameboardBuilder.setTerrain`, `GameboardBuilder.setTileAsset`, `createGameboardBuilder`, `createGameboardLayoutFillRuleFromPiece`, `createGameboardPlanFromRecipe`, `createGameboardPlanFromTiles`, `createGameboardRuntimeFromScenario`, `createMedievalShowcaseBlueprintRecipe`, `createSeededGameboardPlan`, `listRoadGuidePermutations`, `selectRoadVariant`, `selectRoadVariantByLabel`, `selectSpawnCoordinates` * Visual artifacts: `tests/browser/__screenshots__/free-gameboard-recipe.png`, `tests/browser/__screenshots__/free-blueprint-builder-showcase.png`, `tests/browser/__screenshots__/free-seeded-gameboard.png`, `tests/browser/__screenshots__/simple-rpg-fixed-completed.png`, `tests/browser/__screenshots__/extra-blueprint-biome-transition-showcase.png` * Docs: `docs/guides/recipes-scenarios-and-simulation.md`, `docs/pillars/05-koota-runtime-rules.md` ### Page 10 - Floating Islands [Section titled “Page 10 - Floating Islands”](#page-10---floating-islands) * Scenario: `page-10-floating-islands` * Edition: `free` * Source image: `docs/assets/kaykit-guide/pages/page-10.png` * Asset coverage: 45 unique, 45 FREE, 0 EXTRA, 45 occurrences * Roles: `base-tile`, `nature-decoration`, `support-tile` * Public API treatment: `GameboardBuilder.addElevationRamp`, `GameboardBuilder.addForest`, `GameboardBuilder.addHill`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.addNature`, `GameboardBuilder.scatterDecorations`, `GameboardBuilder.setElevation`, `GameboardBuilder.setTileAsset`, `createGameboardBlueprintPlan`, `createGameboardLayoutFillRuleFromPiece`, `createGameboardPlanFromRecipe`, `createHexagonGameboardGrid` * Visual artifacts: `tests/browser/__screenshots__/free-seeded-hex-gameboard.png`, `tests/browser/__screenshots__/free-guide-page-nature-stacks-buildings-props.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/pillars/05-koota-runtime-rules.md` ### Page 11 - Biomes [Section titled “Page 11 - Biomes”](#page-11---biomes) * Scenario: `page-11-biomes` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-11.png` * Asset coverage: 1 unique, 0 FREE, 1 EXTRA, 1 occurrence * Roles: `transition-tile` * Public API treatment: `GameboardBuilder.addTransition`, `createGameboardBlueprintRecipe`, `createGameboardPlanFromRecipe`, `inspectGameboardBlueprint`, `textureFileName`, `validateGameboardRecipe` * Visual artifacts: `tests/browser/__screenshots__/extra-local-all-tiles-guide-and-transitions.png`, `tests/browser/__screenshots__/extra-seasonal-textures.png`, `tests/browser/__screenshots__/extra-blueprint-biome-transition-showcase.png` * Docs: `docs/pillars/03-editions-and-ingest.md`, `docs/guides/rendering-assets-and-external-packs.md` ### Page 12 - Alternate Textures [Section titled “Page 12 - Alternate Textures”](#page-12---alternate-textures) * Scenario: `page-12-alternate-textures` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-12.png` * Asset coverage: 1 unique, 0 FREE, 1 EXTRA, 1 occurrence * Roles: `transition-tile` * Public API treatment: `GameboardBuilder.addTransition`, `createGameboardPlanFromRecipe`, `createManifestBundle`, `declarative-hex-worlds manifest`, `selectManifestAssets`, `textureFileName`, `validateGameboardRecipe` * Visual artifacts: `tests/browser/__screenshots__/extra-seasonal-textures.png`, `tests/browser/__screenshots__/extra-local-all-tiles-guide-and-transitions.png` * Docs: `docs/pillars/03-editions-and-ingest.md`, `docs/guides/rendering-assets-and-external-packs.md` ### Page 13 - Transition Tiles [Section titled “Page 13 - Transition Tiles”](#page-13---transition-tiles) * Scenario: `page-13-transition-tiles` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-13.png` * Asset coverage: 1 unique, 0 FREE, 1 EXTRA, 1 occurrence * Roles: `transition-tile` * Public API treatment: `GameboardBuilder.addTransition`, `analyzeHexTileRegistry`, `createGameboardBlueprintRecipe`, `createGameboardPlanFromRecipe`, `createMedievalShowcaseBlueprintRecipe`, `declareHexTile`, `validateGameboardRecipe`, `validateGameboardRecipeGeneration` * Visual artifacts: `tests/browser/__screenshots__/extra-local-all-tiles-guide-and-transitions.png`, `tests/browser/__screenshots__/extra-seasonal-textures.png`, `tests/browser/__screenshots__/extra-blueprint-biome-transition-showcase.png` * Docs: `docs/pillars/01-tiles-connectivity.md`, `docs/pillars/03-editions-and-ingest.md` ### Page 14 - Units [Section titled “Page 14 - Units”](#page-14---units) * Scenario: `page-14-units` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-14.png` * Asset coverage: 137 unique, 0 FREE, 137 EXTRA, 137 occurrences * Roles: `colored-unit-part`, `neutral-unit-part` * Public API treatment: `GameboardBuilder.addUnit`, `GameboardBuilder.addUnitPreset`, `coloredUnitAssetId`, `neutralUnitAssetId`, `spawnGameboardActor` * Visual artifacts: `tests/browser/__screenshots__/extra-local-all-units-full-accent-neutral-siege.png`, `tests/browser/__screenshots__/simple-rpg-local-third-party-assets.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/guides/runtime-integration.md` ### Page 15 - Shipyard, Harbors, And Ports [Section titled “Page 15 - Shipyard, Harbors, And Ports”](#page-15---shipyard-harbors-and-ports) * Scenario: `page-15-shipyard-harbors` * Edition: `mixed` * Source image: `docs/assets/kaykit-guide/pages/page-15.png` * Asset coverage: 25 unique, 14 FREE, 11 EXTRA, 25 occurrences * Roles: `coast-tile`, `faction-building`, `prop` * Public API treatment: `GameboardBuilder.addFactionBuilding`, `GameboardBuilder.addHarbor`, `GameboardBuilder.addProp`, `GameboardBuilder.addPropCluster`, `GameboardBuilder.addUnitPreset`, `GameboardBuilder.setCoastEdges`, `createGameboardLayoutFillRuleFromPiece`, `externalAssetSpawnOptions`, `factionBuildingAssetId`, `listCoastGuidePermutations`, `listPropClusterAssets`, `selectCoastVariant`, `selectCoastVariantByLabel` * Visual artifacts: `tests/browser/__screenshots__/extra-harbor-gameboard.png`, `tests/browser/__screenshots__/extra-local-all-buildings-factions-neutral-harbors.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/pillars/03-editions-and-ingest.md` ### Page 16 - Stables And Horses [Section titled “Page 16 - Stables And Horses”](#page-16---stables-and-horses) * Scenario: `page-16-stables-and-horses` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-16.png` * Asset coverage: 155 unique, 11 FREE, 144 EXTRA, 155 occurrences * Roles: `colored-unit-part`, `faction-building`, `neutral-structure`, `neutral-unit-part`, `prop` * Public API treatment: `GameboardBuilder.addFactionBuilding`, `GameboardBuilder.addFortification`, `GameboardBuilder.addNeutralStructure`, `GameboardBuilder.addProp`, `GameboardBuilder.addPropCluster`, `GameboardBuilder.addSettlement`, `GameboardBuilder.addUnit`, `GameboardBuilder.addUnitPreset`, `coloredUnitAssetId`, `createGameboardLayoutFillRuleFromPiece`, `createGameboardPlanFromRecipe`, `factionBuildingAssetId`, `listPropClusterAssets`, `neutralUnitAssetId`, `recommendExternalAssetFacing`, `spawnGameboardActor` * Visual artifacts: `tests/browser/__screenshots__/extra-local-all-units-full-accent-neutral-siege.png`, `tests/browser/__screenshots__/extra-local-all-decoration-nature-props.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/guides/rendering-assets-and-external-packs.md` ### Page 17 - Workshop And Siege Units [Section titled “Page 17 - Workshop And Siege Units”](#page-17---workshop-and-siege-units) * Scenario: `page-17-workshop-and-siege` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-17.png` * Asset coverage: 170 unique, 22 FREE, 148 EXTRA, 170 occurrences * Roles: `colored-unit-part`, `faction-building`, `neutral-structure`, `neutral-unit-part`, `prop` * Public API treatment: `GameboardBuilder.addConstructionSite`, `GameboardBuilder.addFactionBuilding`, `GameboardBuilder.addFortification`, `GameboardBuilder.addNeutralStructure`, `GameboardBuilder.addProp`, `GameboardBuilder.addPropCluster`, `GameboardBuilder.addSettlement`, `GameboardBuilder.addSiegeProjectile`, `GameboardBuilder.addUnit`, `GameboardBuilder.addUnitPreset`, `coloredUnitAssetId`, `createGameboardLayoutFillRuleFromPiece`, `createGameboardPlanFromRecipe`, `executeGameboardInteractionCommand`, `factionBuildingAssetId`, `listPropClusterAssets`, `neutralUnitAssetId`, `planGameboardInteractionCommand`, `spawnGameboardActor` * Visual artifacts: `tests/browser/__screenshots__/extra-local-all-buildings-factions-neutral-harbors.png`, `tests/browser/__screenshots__/extra-local-all-units-full-accent-neutral-siege.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/guides/runtime-integration.md` ### Page 18 - Unit Combinations [Section titled “Page 18 - Unit Combinations”](#page-18---unit-combinations) * Scenario: `page-18-unit-combinations` * Edition: `extra` * Source image: `docs/assets/kaykit-guide/pages/page-18.png` * Asset coverage: 137 unique, 0 FREE, 137 EXTRA, 137 occurrences * Roles: `colored-unit-part`, `neutral-unit-part` * Public API treatment: `GameboardBuilder.addUnit`, `GameboardBuilder.addUnitPreset`, `coloredUnitAssetId`, `createGameboardRuntimeFromScenario`, `neutralUnitAssetId`, `spawnGameboardActor` * Visual artifacts: `tests/browser/__screenshots__/extra-local-all-units-full-accent-neutral-siege.png`, `tests/browser/__screenshots__/simple-rpg-seeded-completed.png` * Docs: `docs/pillars/02-asset-taxonomy.md`, `docs/guides/recipes-scenarios-and-simulation.md` ### Page 19 - Supporters And Attribution [Section titled “Page 19 - Supporters And Attribution”](#page-19---supporters-and-attribution) * Scenario: `page-19-supporters-and-attribution` * Edition: `reference` * Source image: `docs/assets/kaykit-guide/pages/page-19.png` * Asset coverage: 0 unique, 0 FREE, 0 EXTRA, 0 occurrences * Roles: reference-only * Public API treatment: `NOTICE.md`, `listKayKitGuideScenarios`, `package.json files` * Visual artifacts: `docs/assets/kaykit-guide/pages/page-19.png`, `NOTICE.md` * Docs: `NOTICE.md`, `docs/pillars/00-library-charter.md`, `README.md` ## Asset Coverage Query [Section titled “Asset Coverage Query”](#asset-coverage-query) The asset index starts from a manifest id and reports the exact role, public API surface, source images, docs, and screenshots that keep that GLTF from being a passive file reference: ```ts import { describeKayKitGuideAssetCoverage, listKayKitGuideAssetCoverages, } from 'declarative-hex-worlds/catalog'; const roadM = describeKayKitGuideAssetCoverage('hex_road_M'); const allAssetCoverage = listKayKitGuideAssetCoverages(); ``` ## Page-Level Usage Query [Section titled “Page-Level Usage Query”](#page-level-usage-query) The usage index preserves repeated page-level asset occurrences for contact sheets, renderer tests, and audit tools that need the exact FREE/EXTRA guide scenario workload instead of only unique coverage rows: ```ts import { listKayKitGuideScenarioAssetRenderGroups, listKayKitGuideScenarioAssetRenderRequests, listKayKitGuideScenarioAssetUsages, listKayKitGuideScenarioAssetUsagesForScenario, } from 'declarative-hex-worlds/catalog'; const freeGuideAssets = listKayKitGuideScenarioAssetUsages({ minimumEdition: "free" }); const stableWorkshopUnits = listKayKitGuideScenarioAssetUsages({ pages: [16, 17, 18] }); const freeRenderQueue = listKayKitGuideScenarioAssetRenderRequests({ minimumEdition: "free", assetBaseUrl: "/assets/free", }); const groupedRenderQueue = listKayKitGuideScenarioAssetRenderGroups({ pages: [16, 17, 18] }); const page14Units = listKayKitGuideScenarioAssetUsagesForScenario('page-14-units'); ``` ## Role Coverage Index [Section titled “Role Coverage Index”](#role-coverage-index) The role index starts from a gameplay use case and reports every guide page, asset id, public API, doc, and screenshot that exercises that role: ```ts import { describeKayKitGuideRoleCoverage, listKayKitGuideRoleCoverages, } from 'declarative-hex-worlds/catalog'; const props = describeKayKitGuideRoleCoverage('prop'); const allRoleCoverage = listKayKitGuideRoleCoverages(); ``` ### Role - `base-tile` [Section titled “Role - base-tile”](#role---base-tile) * Pages: 07, 08, 09, 10 * Asset coverage: 4 unique, 4 FREE, 0 EXTRA, 8 occurrences * Public API treatment: `GameboardBuilder.addElevationRamp`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.setTerrain`, `GameboardBuilder.setTileAsset`, `createGameboardPlanFromRecipe`, `createGameboardPlanFromTiles` * Scenarios: `page-07-water-usage`, `page-08-taller-hex-tiles`, `page-09-world-design-example`, `page-10-floating-islands` ### Role - `coast-tile` [Section titled “Role - coast-tile”](#role---coast-tile) * Pages: 07, 15 * Asset coverage: 10 unique, 10 FREE, 0 EXTRA, 20 occurrences * Public API treatment: `GameboardBuilder.addHarbor`, `GameboardBuilder.setCoastEdges`, `listCoastGuidePermutations`, `selectCoastVariant`, `selectCoastVariantByLabel` * Scenarios: `page-07-water-usage`, `page-15-shipyard-harbors` ### Role - `colored-unit-part` [Section titled “Role - colored-unit-part”](#role---colored-unit-part) * Pages: 14, 16, 17, 18 * Asset coverage: 112 unique, 0 FREE, 112 EXTRA, 448 occurrences * Public API treatment: `GameboardBuilder.addUnit`, `GameboardBuilder.addUnitPreset`, `coloredUnitAssetId`, `spawnGameboardActor` * Scenarios: `page-14-units`, `page-16-stables-and-horses`, `page-17-workshop-and-siege`, `page-18-unit-combinations` ### Role - `faction-building` [Section titled “Role - faction-building”](#role---faction-building) * Pages: 02, 15, 16, 17 * Asset coverage: 108 unique, 72 FREE, 36 EXTRA, 132 occurrences * Public API treatment: `GameboardBuilder.addFactionBuilding`, `GameboardBuilder.addHarbor`, `GameboardBuilder.addSettlement`, `factionBuildingAssetId` * Scenarios: `page-02-buildings-props-and-factions`, `page-15-shipyard-harbors`, `page-16-stables-and-horses`, `page-17-workshop-and-siege` ### Role - `nature-decoration` [Section titled “Role - nature-decoration”](#role---nature-decoration) * Pages: 05, 06, 09, 10 * Asset coverage: 42 unique, 42 FREE, 0 EXTRA, 168 occurrences * Public API treatment: `GameboardBuilder.addForest`, `GameboardBuilder.addHill`, `GameboardBuilder.addMountainStack`, `GameboardBuilder.addNature`, `GameboardBuilder.scatterDecorations`, `createGameboardLayoutFillRuleFromPiece` * Scenarios: `page-05-nature-contents`, `page-06-nature-usage`, `page-09-world-design-example`, `page-10-floating-islands` ### Role - `neutral-structure` [Section titled “Role - neutral-structure”](#role---neutral-structure) * Pages: 02, 07, 09, 16, 17 * Asset coverage: 21 unique, 21 FREE, 0 EXTRA, 55 occurrences * Public API treatment: `GameboardBuilder.addBridge`, `GameboardBuilder.addConstructionSite`, `GameboardBuilder.addFortification`, `GameboardBuilder.addNeutralStructure`, `GameboardBuilder.addSiegeProjectile`, `createGameboardPlanFromRecipe` * Scenarios: `page-02-buildings-props-and-factions`, `page-07-water-usage`, `page-09-world-design-example`, `page-16-stables-and-horses`, `page-17-workshop-and-siege` ### Role - `neutral-unit-part` [Section titled “Role - neutral-unit-part”](#role---neutral-unit-part) * Pages: 14, 16, 17, 18 * Asset coverage: 25 unique, 0 FREE, 25 EXTRA, 100 occurrences * Public API treatment: `GameboardBuilder.addUnit`, `GameboardBuilder.addUnitPreset`, `neutralUnitAssetId`, `spawnGameboardActor` * Scenarios: `page-14-units`, `page-16-stables-and-horses`, `page-17-workshop-and-siege`, `page-18-unit-combinations` ### Role - `prop` [Section titled “Role - prop”](#role---prop) * Pages: 02, 05, 15, 16, 17 * Asset coverage: 35 unique, 26 FREE, 9 EXTRA, 82 occurrences * Public API treatment: `GameboardBuilder.addFlag`, `GameboardBuilder.addHarbor`, `GameboardBuilder.addProp`, `GameboardBuilder.addPropCluster`, `createGameboardLayoutFillRuleFromPiece`, `flagAssetId`, `listPropClusterAssets` * Scenarios: `page-02-buildings-props-and-factions`, `page-05-nature-contents`, `page-15-shipyard-harbors`, `page-16-stables-and-horses`, `page-17-workshop-and-siege` ### Role - `river-tile` [Section titled “Role - river-tile”](#role---river-tile) * Pages: 04, 07 * Asset coverage: 30 unique, 30 FREE, 0 EXTRA, 60 occurrences * Public API treatment: `GameboardBuilder.addRiverPath`, `listRiverCrossingGuidePermutations`, `listRiverCurvyGuidePermutations`, `listRiverGuidePermutations`, `selectRiverCrossingVariant`, `selectRiverVariant`, `selectRiverVariantByLabel` * Scenarios: `page-04-river-variations`, `page-07-water-usage` ### Role - `road-tile` [Section titled “Role - road-tile”](#role---road-tile) * Pages: 03, 09 * Asset coverage: 15 unique, 15 FREE, 0 EXTRA, 30 occurrences * Public API treatment: `GameboardBuilder.addRoadPath`, `listRoadGuidePermutations`, `selectRoadVariant`, `selectRoadVariantByLabel` * Scenarios: `page-03-road-variations`, `page-09-world-design-example` ### Role - `support-tile` [Section titled “Role - support-tile”](#role---support-tile) * Pages: 08, 10 * Asset coverage: 1 unique, 1 FREE, 0 EXTRA, 2 occurrences * Public API treatment: `GameboardBuilder.addMountainStack`, `GameboardBuilder.setTileAsset`, `createGameboardPlanFromRecipe` * Scenarios: `page-08-taller-hex-tiles`, `page-10-floating-islands` ### Role - `transition-tile` [Section titled “Role - transition-tile”](#role---transition-tile) * Pages: 11, 12, 13 * Asset coverage: 1 unique, 0 FREE, 1 EXTRA, 3 occurrences * Public API treatment: `GameboardBuilder.addTransition`, `createGameboardPlanFromRecipe`, `validateGameboardRecipe` * Scenarios: `page-11-biomes`, `page-12-alternate-textures`, `page-13-transition-tiles` ## Public API Inversion [Section titled “Public API Inversion”](#public-api-inversion) The page matrix flows from guide page to public API. The inverse query starts from an API surface and reports every guide page, asset id, role, doc, and screenshot that exercises it: ```ts import { describeKayKitGuidePublicApiCoverage, listKayKitGuidePublicApiCoverages, } from 'declarative-hex-worlds/catalog'; const harbor = describeKayKitGuidePublicApiCoverage('GameboardBuilder.addHarbor'); const allApiCoverage = listKayKitGuidePublicApiCoverages(); ``` For example, `GameboardBuilder.addHarbor` maps to pages 02, 05, 07, and 15, covering coast tiles, faction buildings, and props across FREE and EXTRA source material. `GameboardBuilder.addBridge` maps to pages 02, 07, and 09, covering the FREE bridge structures used by road and water crossings. `GameboardBuilder.addElevationRamp` maps to pages 08 and 10, covering the FREE sloped grass tiles used by vertical terrain transitions. `GameboardBuilder.addFortification`, `GameboardBuilder.addConstructionSite`, and `GameboardBuilder.addSiegeProjectile` cover the remaining FREE neutral wall, fence, construction, ruin, and projectile structures as authored gameplay intent instead of only raw neutral placements. `GameboardBuilder.addPropCluster` maps non-flag props to camps, resource caches, worksites, training yards, stable yards, and harbor support dressing with density, single-tile stacking, adjacent spread, and local EXTRA opt-in. `GameboardBuilder.addUnitPreset` maps to pages 14 through 18 and is EXTRA-only because the unit assembly pieces are local-ingest assets. # KayKit Pack Upstream Layout > Authoritative reference for the on-disk shape of the KayKit Medieval Hexagon Pack as bootstrapped by `declarative-hex-worlds`. `declarative-hex-worlds` does not bundle KayKit asset binaries. The CLI `bootstrap` subcommand (and the equivalent `bootstrapKayKitAssets` programmatic API) mirrors the upstream KayKit pack tree into the consumer’s asset root at install time. This page is the authoritative description of the upstream layout the bootstrap step understands. ## Editions [Section titled “Editions”](#editions) Two editions are supported: | Edition | License | Source | | ------- | ------------------ | -------------------------------------------------------------------------------------------------------------------- | | `free` | CC0-1.0 | [KayKit-Medieval-Hexagon-Pack-1.0](https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0) on GitHub | | `extra` | CC0-1.0 (purchase) | [`kaylousberg.itch.io`](https://kaylousberg.itch.io) — supplied via `--source zip` | ## FREE edition layout [Section titled “FREE edition layout”](#free-edition-layout) Pack folder: `KayKit_Medieval_Hexagon_Pack_1.0_FREE/` ```text KayKit_Medieval_Hexagon_Pack_1.0_FREE/ ├── Assets/ │ ├── gltf/ # mirrored by bootstrap │ │ ├── buildings/{blue,green,red,yellow,neutral}/ │ │ ├── decoration/{nature,props}/ │ │ └── tiles/{base,coast,rivers,roads}/ │ ├── fbx/ # ignored unless --include-source-formats │ ├── fbx(unity)/ # ignored unless --include-source-formats │ └── obj/ # ignored unless --include-source-formats ├── Textures/ │ └── hexagons_medieval.png ├── Samples/ │ ├── sample1.jpg │ └── sample2.jpg ├── License.txt ├── Medieval_Hexagon_UserGuide_v1.pdf ├── contents_buildings.jpg ├── contents_nature.jpg └── contents_tiles.jpg ``` * **GLTF count:** 221 (one `.bin` companion each). * **Categories:** `buildings`, `decoration`, `tiles`. * **Texture sets:** `default` only. ## EXTRA edition layout [Section titled “EXTRA edition layout”](#extra-edition-layout) Pack folder: `KayKit_Medieval_Hexagon_Pack_1.0_EXTRA/` ```text KayKit_Medieval_Hexagon_Pack_1.0_EXTRA/ ├── Assets/ │ ├── gltf/ │ │ ├── buildings/{blue,green,red,yellow,neutral}/ │ │ ├── decoration/{nature,props}/ │ │ ├── tiles/{base,coast,rivers,roads}/ │ │ └── units/{blue,green,red,yellow}/ # EXTRA-only category │ ├── fbx/, fbx(unity)/, obj/ # ignored unless --include-source-formats ├── Textures/ │ ├── hexagons_medieval.png │ ├── hexagons_medieval_Fall.png # EXTRA-only seasonal texture │ ├── hexagons_medieval_Summer.png # EXTRA-only │ └── hexagons_medieval_Winter.png # EXTRA-only ├── License.txt ├── Medieval_Hexagon_UserGuide_v1.pdf ├── contents_buildings.jpg ├── contents_nature.jpg ├── contents_tiles.jpg ├── contents_textures.jpg # EXTRA-only marker └── contents_units.jpg # EXTRA-only marker ``` * **GLTF count:** 404 (one `.bin` companion each). * **Categories:** `buildings`, `decoration`, `tiles`, `units`. * **Texture sets:** `default`, `fall`, `summer`, `winter`. ## Bootstrap target layout [Section titled “Bootstrap target layout”](#bootstrap-target-layout) Both editions are mirrored under the same path on the consumer: ```text /addons/kaykit_medieval_hexagon_pack/ ├── Assets/gltf/... # mirror of the upstream gltf tree └── .bootstrap.json # integrity sidecar ``` `` defaults to `public/assets/models/` (Vite / Next.js style), falling back to `assets/models/` and then the current working directory. It can be overridden with `--out` on the CLI or `BootstrapKayKitAssetsOptions.out` in the programmatic API. ## Edition detection [Section titled “Edition detection”](#edition-detection) The bootstrap step (and the programmatic [`detectKayKitLayout`](https://jsr.io/declarative-hex-worlds) helper) identifies an edition by checking marker files plus the presence of the `units/` directory: | Marker / signal | FREE | EXTRA | | ----------------------------------- | ---- | ----- | | `License.txt` | yes | yes | | `Medieval_Hexagon_UserGuide_v1.pdf` | yes | yes | | `contents_buildings.jpg` | yes | yes | | `contents_nature.jpg` | yes | yes | | `contents_tiles.jpg` | yes | yes | | `contents_units.jpg` | no | yes | | `contents_textures.jpg` | no | yes | | `Assets/gltf/units/` | no | yes | EXTRA is tested first because its marker set is a superset of FREE’s. ## Programmatic surface [Section titled “Programmatic surface”](#programmatic-surface) ```ts import { KAYKIT_MEDIEVAL_FREE_LAYOUT, KAYKIT_MEDIEVAL_EXTRA_LAYOUT, detectKayKitLayout, kayKitLayoutForEdition, expectedTexturePaths, type KayKitUpstreamLayout, } from 'declarative-hex-worlds/bootstrap/upstream-layout'; ``` Use these to: * Pre-flight check an extracted pack folder (`detectKayKitLayout(rootPath)`). * Drive consumer-side validation pipelines that need the published asset counts (`KAYKIT_MEDIEVAL_FREE_LAYOUT.expectedGltfCount`). * List the texture filenames an edition should have shipped (`expectedTexturePaths(rootPath, layout)`). # Live demos > In-browser boards rendered through the declarative-hex-worlds renderer bindings — the docs run the library, they don't just describe it. The signals-and-bindings architecture lets one world drive any renderer. These demos render the **same** reference example game — a deterministic world compiled from a JSON scenario into a live Koota runtime — through different renderer bindings, live in your browser. ## Available demos [Section titled “Available demos”](#available-demos) * **[Canvas-2D binding →](/demo/canvas2d/)** — the game drawn as 2D sprites through `declarative-hex-worlds/canvas2d`. Zero renderer dependency and no downloaded art (the sprite sheet is generated procedurally), so it runs anywhere a 2D canvas exists. ## How it works [Section titled “How it works”](#how-it-works) The core (`declarative-hex-worlds`) is renderer-free: it emits reactive Koota-trait *signals* — each placement’s position, asset, and dimension. A renderer *binding* subscribes to those signals and reconciles its own scene: * `declarative-hex-worlds/canvas2d` turns each `{ dimension: '2d' }` placement into a sprite blit on a `CanvasRenderingContext2D`. * `declarative-hex-worlds/three` takes the *same* signals and reconciles a Three.js scene for `{ dimension: '3d' }` GLTF models. Both consume the identical shared game (`@declarative-hex-worlds/examples`), proving the render seam is genuinely substrate-agnostic. A Pixi binding would be the same shape with `PIXI.Sprite` in place of `drawImage`. # Migration Guide > How to upgrade between declarative-hex-worlds versions and migrate from the pre-release package name. ## From the pre-release scoped package (`@jbcom/medieval-hexagon-gameboard`) [Section titled “From the pre-release scoped package (@jbcom/medieval-hexagon-gameboard)”](#from-the-pre-release-scoped-package-jbcommedieval-hexagon-gameboard) Before the 1.0.0 release, the library was distributed under the internal scoped name `@jbcom/medieval-hexagon-gameboard`. The public 1.0.0 release renamed it to the unscoped `declarative-hex-worlds`. ### Step 1 — Update your `package.json` dependency [Section titled “Step 1 — Update your package.json dependency”](#step-1--update-your-packagejson-dependency) ```diff "@jbcom/medieval-hexagon-gameboard": "^0.x.x" "declarative-hex-worlds": "^1.0.0" ``` ### Step 2 — Update all import statements [Section titled “Step 2 — Update all import statements”](#step-2--update-all-import-statements) ```diff import { createGameboard } from '@jbcom/medieval-hexagon-gameboard'; import { createGameboard } from 'declarative-hex-worlds'; ``` Subpath imports follow the same pattern: ```diff import { hexKey } from '@jbcom/medieval-hexagon-gameboard/coordinates'; import { hexKey } from 'declarative-hex-worlds/coordinates'; ``` ### Step 3 — Update the CLI binary name [Section titled “Step 3 — Update the CLI binary name”](#step-3--update-the-cli-binary-name) The CLI binary was also renamed: ```diff npx @jbcom/medieval-hexagon-gameboard npx declarative-hex-worlds ``` Or via pnpm exec: ```diff pnpm exec medieval-hexagon-gameboard pnpm exec declarative-hex-worlds ``` ### Step 4 — Update environment variables [Section titled “Step 4 — Update environment variables”](#step-4--update-environment-variables) The output jail environment variable was renamed: ```diff MEDIEVAL_HEXAGON_OUT_ROOT=/path/to/output HEX_WORLDS_OUT_ROOT=/path/to/output ``` ### What did NOT change [Section titled “What did NOT change”](#what-did-not-change) * The published API surface — all exported functions, types, and constants are identical. * The subpath structure — all `/coordinates`, `/gameboard`, `/scenario`, etc. subpaths are preserved. * The asset format — existing recipe JSON files, scenario files, and manifest files are fully compatible. ## From 1.0.x to 1.1.x [Section titled “From 1.0.x to 1.1.x”](#from-10x-to-11x) No breaking changes. Patch releases only. ## API versioning policy [Section titled “API versioning policy”](#api-versioning-policy) Starting from 1.0.0, this package follows [Semantic Versioning](https://semver.org): * **Patch** (`1.0.x`): bug fixes, documentation, internal optimizations. * **Minor** (`1.x.0`): new exports, new subpaths, additive API changes. * **Major** (`x.0.0`): breaking changes to existing exports, removed subpaths, renamed types. Breaking changes will always be documented here with a concrete migration path before they ship. # Public API tier table > Three-tier export taxonomy (umbrella / domain subpath / internal). `declarative-hex-worlds` publishes one umbrella entry plus a wide set of subpath exports. **Every subpath stays supported through 1.0** — this is an asset-bundled library where mod authors, custom-renderer builders, and data-inspection tooling all have legitimate reasons to reach internals. What changes between tiers is the **stability contract**, not the availability. Pick the tier that matches your tolerance for breakage; the tooling and TSDoc tags tell you which tier a symbol belongs to. ## Tier 1 — Stable [Section titled “Tier 1 — Stable”](#tier-1--stable) Semver-strict. Breaking changes only on major versions, always with a migration guide. | Subpath | Purpose | | ------------------- | ------------------------------------------------------------------------------------------------- | | `.` (umbrella) | Default entry. Re-exports the consumer-facing surface from every tier-1 sub-package. | | `./react` | React bindings. First-class (NOT peer-gated). Hooks, providers, components. | | `./three` | Three.js bindings. First-class (NOT peer-gated). Loaders, disposers, scene helpers. | | `./cli` | CLI entry (also installed as the `declarative-hex-worlds` bin). | | `./manifest/schema` | `MedievalHexagonManifest` shape + validators. | | `./manifest/free` | Pre-baked FREE-edition manifest metadata. | | `./scenario` | `GameboardScenario`, blueprint/recipe/catalog/registry. | | `./blueprint` | `GameboardBlueprintOptions` and procedural board generation. | | `./gameboard` | Board lifecycle, occupancy, navigation. | | `./recipe` | Recipe DSL. | | `./coverage` | Release-readiness coverage ledger surface for build/review tooling, not runtime ECS adapter glue. | | `./compatibility` | Manifest/version compatibility helpers. | | `./errors` | `GameboardError` + typed subclasses (Epic D2). | | `./traits` | Single trait umbrella (all `trait()` declarations). | | `./types` | Shared primitive types + branded IDs (`HexKey`, `ActorId`, etc.). | | `./examples/*.json` | Bundled example scenario JSON. | TSDoc tag: `@public`. ## Branded ID migration status [Section titled “Branded ID migration status”](#branded-id-migration-status) The `./types` subpath exports branded ID aliases and `brand*` helpers, but branded IDs are **NOT yet enforced** across the runtime. Public JSON, manifest, CLI, and ECS-facing shapes still use plain strings unless a domain row below states that enforcement has landed. Treat the aliases as opt-in compile-time helpers until the relevant domain moves to `enforced`. | Domain(s) | Brands tracked | Current status | | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `./types` | `HexKey`, `ActorId`, `TileId`, `PieceId`, `PlacementId`, `ScenarioId`, `QuestId`, `ObjectiveId`, `PatrolRouteId`, `AssetId` | Registry exported; `brand*` helpers are stable and available to consumers. | | `./coordinates`, `./grid`, `./layout`, `./gameboard` | `HexKey`, `TileId` | Not yet enforced; public APIs still accept coordinate objects and plain string IDs. | | `./scenario`, `./recipe`, `./blueprint`, `./simulation`, `./pieces` | `ScenarioId`, `PieceId`, `PlacementId`, `TileId`, `AssetId` | Not yet enforced; scenario JSON and generated plans keep wire-compatible string fields. | | `./actors`, `./movement`, `./patrol`, `./quests` | `ActorId`, `PatrolRouteId`, `QuestId`, `ObjectiveId` | Not yet enforced; ECS traits, commands, and query helpers still expose string-compatible IDs. | | `./manifest/schema`, `./manifest/free`, `./ingest`, `./runtime`, `./react`, `./three` | `AssetId` | Not yet enforced; asset IDs remain manifest and URL-facing strings. | | `./cli`, `./interop`, `./compatibility`, `./coverage` | Pass-through IDs only | Not yet enforced; these surfaces preserve external wire formats and report existing domain values. | ## Tier 2 — Supported-for-extension [Section titled “Tier 2 — Supported-for-extension”](#tier-2--supported-for-extension) Semver-strict for what’s documented, but the surface contract is smaller than tier 1. Mod authors and custom-renderer consumers can depend on these; breaking changes still require a major + migration guide but the “what’s breaking” frame may be narrower (a specific function signature changing rather than the whole module shape). | Subpath | Purpose | | --------------- | -------------------------------------------------------------- | | `./koota` | World bootstrap, trait sets, query patterns. | | `./runtime` | High-level runtime composition. | | `./actors` | Actor surface + queries. | | `./movement` | Movement-agent surface. | | `./patrol` | Patrol-route surface. | | `./quests` | Quest surface. | | `./pieces` | Piece declarations + placement helpers. | | `./projection` | World-space placement projection. | | `./layout` | Seeded board layout generation. | | `./grid` | Honeycomb-grid wrappers + hex algebra. | | `./coordinates` | Hex coordinate algebra (umbrella over grid/projection/layout). | | `./validation` | Plan-level validators. | | `./rules` | Rule definitions + evaluation. | TSDoc tag: `@public` (no distinct tag — tier 2 is documented here in the table, not on every symbol). ## Tier 3 — Internal-but-exposed [Section titled “Tier 3 — Internal-but-exposed”](#tier-3--internal-but-exposed) Semver-**permissive**: minor versions may change these surfaces in non-trivial ways. Consumers who pin tier-3 imports accept this tradeoff. Useful for very-deep modding, debugging, and tools that need to inspect or hook the implementation. | Subpath | Purpose | | --------------- | ----------------------------------------------------------------------------------------------------------------------- | | `./commands` | Internal command-factory plumbing. | | `./selectors` | Per-render shape pickers used by React/Three bindings. | | `./systems` | Tickable system functions. | | `./world-rules` | Runtime rule-evaluation system. | | `./rule-types` | Rule typed shapes (re-exported from `./rules` — prefer that). | | `./interop` | Runtime ECS adapter glue and schema migrations; release-readiness coverage is the separate stable `./coverage` surface. | | `./ingest` | Source-tree walker + manifest emission (precursor to bootstrap). | | `./registry` | Tile/piece registries (re-exported from `./scenario`). | | `./catalog` | KayKit asset catalog (re-exported from `./scenario`). | TSDoc tag: `@internal` (still exported, still typed, still tested — just flagged so consumers see the support tier inline). ## Why the three tiers? [Section titled “Why the three tiers?”](#why-the-three-tiers) Phase 1’s architecture review (F1) noted that the 37+ subpath exports permanently pin every internal module as a semver surface point. The original recommendation was to demote \~10 subpaths and force everything through the umbrella. Per PRD §4 Epic D1 (re-scoped), that was rejected: this library bundles the FREE KayKit pack so consumers get a working game out-of-box, and the “out-of-box” promise extends to consumers who want to reach internals for legitimate reasons (custom renderers, modding, data inspection). The right tradeoff is **documented tiering**, not gated demotion. If a tier-3 surface stabilizes (a custom-renderer mod has been depending on `./selectors` for two minor versions without breakage), it can be promoted to tier 2 via a CHANGELOG note. The reverse (demoting a tier-1 surface) is a major-version event. ## Trait identity invariant [Section titled “Trait identity invariant”](#trait-identity-invariant) Regardless of tier, every `trait()` declaration is exported from **exactly one module**. `tsup`’s `splitting: true` keeps shared chunks stable so that two consumers importing `GameboardActor` from different subpaths (e.g. one via `./actors`, another via the umbrella) get the SAME trait identity — which is what koota’s `useQuery` reference-equality lookups depend on. Don’t try to “optimize” by re-declaring a trait somewhere else. Don’t fork the trait file into a “local” copy. The trait-identity test (Epic E4, pending) pins this. # Recipes, scenarios, and simulation > The recipe → blueprint → scenario → simulation pipeline. Recipes describe board intent. Scenarios add game content. Simulation scripts exercise the same public API a game loop would use. ## Recipes [Section titled “Recipes”](#recipes) A recipe is the smallest useful serialized authoring format. It can contain: * a board shape and seed. * explicit tile or placement steps. * layout archetypes local to the recipe. * generated `layoutFills` for built-in archetypes. * generated `pieceDeclarations` and `pieceFills` for custom packs. * manifest validation requirements. Use recipes when a board needs to be saved, generated by tools, checked into a game repository, or reviewed in documentation. ```ts import { createGameboardPlanFromRecipe, inspectGameboardRecipe, } from 'declarative-hex-worlds/recipe'; import { freeManifest } from 'declarative-hex-worlds/manifest/free'; const preflight = inspectGameboardRecipe(recipeJson, { plan: { assetCatalog: freeManifest }, }); const recipeErrors = preflight.violations.filter((issue) => issue.severity === 'error'); if (recipeErrors.length > 0) { throw new Error(recipeErrors.map((issue) => issue.message).join('\n')); } const plan = createGameboardPlanFromRecipe(recipeJson); ``` Recipe generation should be deterministic. Use one seed for board terrain and a separate seed namespace for layout fills, piece fills, spawn groups, or patrols when a game needs stable diffs. ## Layout And Piece Fills [Section titled “Layout And Piece Fills”](#layout-and-piece-fills) Layout fills are the core random-board primitive. They combine a placement role, candidate-site criteria, density or count, and deterministic scoring. Common cases: * trees use scatter or tree archetypes, often with `fill`, `maxCount`, `maxPerTile`, and a shared slot group. * harbors use coast tiles adjacent to water. * landmarks and towers use larger footprints and occupancy reservations. * units use spawn-like criteria and usually remain low-density. * loose props can allow occupied tiles if they use a non-blocking slot group. Piece fills are the custom-pack version of layout fills. They select reusable piece declarations by id, asset id, role, source, tag, or local-only state, then expand the selection into deterministic layout rules. Run analysis before committing generated placements into a plan or runtime: ```ts import { analyzeGameboardLayoutFill } from 'declarative-hex-worlds/layout'; import { inspectSeededGameboardPieceFills } from 'declarative-hex-worlds/rules'; const layoutReport = analyzeGameboardLayoutFill(plan, layoutFill); const pieceReport = inspectSeededGameboardPieceFills(plan, registry, pieceFills, { seed: 'campaign-01:custom-pieces', }); ``` Warnings are useful, not cosmetic. A low candidate count, clamped fill count, or large footprint rejection usually means the authored board cannot support the requested density. `createGameboardLayoutPlacements` — the single-placement path used for structures, landmarks, and other one-off pieces — returns an empty array with no warnings when its resolved criteria match zero sites. This is easy to misread as “the board is full” when the real cause is an archetype-inherited criteria field the caller never set (for example, the `landmark` archetype defaults `edgePadding` to `1`, which silently excludes edge tiles a caller’s override criteria did not think to re-enable). Pass `onDiagnostics` to see the fully resolved criteria and a rejection histogram whenever the selected count falls short of the requested count: ```ts import { createGameboardLayoutPlacements } from 'declarative-hex-worlds/layout'; const placements = createGameboardLayoutPlacements(plan, { archetype: 'landmark', count: 1, assetId: 'obelisk', criteria: { terrain: 'grass' }, // edgePadding: 1 is still inherited from the archetype onDiagnostics: (diagnostics) => { console.warn( `landmark placement short by ${diagnostics.requestedCount - diagnostics.selectedCount}`, diagnostics.resolvedCriteria, // shows the inherited edgePadding diagnostics.rejectionCounts // names which filter emptied the candidate set ); }, }); ``` `onDiagnostics` is only invoked when `selectedCount` is less than the requested `count`; omitting it is a no-op with identical placement output. ## Scenarios [Section titled “Scenarios”](#scenarios) A scenario combines a recipe with gameplay objects: * named spawn groups. * patrol route plans. * actors, including players, NPCs, enemies, props, blockers, and interactives. * movement agents and movement profiles. * patrol agents. * quests. * renderer source URL maps for local custom pieces. Scenarios are the preferred test fixture for integration and browser coverage because they force consumers to use the package the way a game does. ```ts import { createGameboardRuntimeFromScenario } from 'declarative-hex-worlds/runtime'; import { validateGameboardScenario } from 'declarative-hex-worlds/scenario'; const validation = validateGameboardScenario(scenarioJson, { plan: { assetCatalog: manifest }, }); const scenarioErrors = validation.filter((issue) => issue.severity === 'error'); if (scenarioErrors.length > 0) { throw new Error(scenarioErrors.map((issue) => issue.message).join('\n')); } const runtime = createGameboardRuntimeFromScenario(scenarioJson); ``` Scenario validation should fail duplicate ids, unresolved spawn groups, duplicate spawn-location claims, missing patrol routes, broken actor references, broken quest references, missing manifest assets, and invalid runtime placement requests. ## Simulation Scripts [Section titled “Simulation Scripts”](#simulation-scripts) Simulation scripts are deterministic headless game-loop tests. They can run: * movement commands. * `run-systems` ticks for patrols, movement, commands, actor targets, and quests. * command previews and command execution. * actor-target inspections and chosen-target dispatch. * actor spawn, update, and removal mutations. * placement spawn, update, move, and removal mutations. * expectation checks against actors, quests, movement, events, commands, and final placement records. ```ts import { runGameboardScenarioSimulationScript, validateGameboardScenarioSimulationScript, } from 'declarative-hex-worlds/simulation'; const scriptValidation = validateGameboardScenarioSimulationScript(scriptJson, { scenario: scenarioJson, }); const simulationErrors = scriptValidation.filter((issue) => issue.severity === 'error'); if (simulationErrors.length > 0) { throw new Error(simulationErrors.map((issue) => issue.message).join('\n')); } const result = runGameboardScenarioSimulationScript(scenarioJson, scriptJson); ``` The `SimpleRPG` fixtures in the package are the canonical acceptance shape. They spawn a player, traverse a fixed board, classify props and enemies through public actor APIs, advance quests, and exercise seeded content through the same scenario and simulation layer. The packaged usage example also exposes `summarizeSimpleRpgGuidePublicApiExercises()`, which joins every current guide-facing public API to SimpleRPG evidence so app smoke tests can fail on missing or stale guide/API representation. Evidence modes are memberships, not exclusive buckets, so one API can be executable smoke and also prove seeded generation, blueprint compilation, manifest packaging, compatibility adapters, or visual coverage. Its executable helper smoke directly invokes 40 guide-facing helper APIs and checks the 404 KayKit public treatment records plus all 19 decomposed guide pages, so catalog coverage is exercised through package imports as well as the generated ledger. ## CLI Flow [Section titled “CLI Flow”](#cli-flow) Use the CLI to preflight serialized content outside a test runner: ```bash declarative-hex-worlds validate-recipe \ --recipe docs/examples/generated-piece-scenario.recipe.json \ --outPlan /tmp/generated-piece-scenario.plan.json declarative-hex-worlds validate-scenario \ --scenario \ --manifest assets/free/manifest.json \ --outPlan /tmp/simple-rpg-plan.json declarative-hex-worlds validate-simulation \ --scenario \ --script \ --manifest assets/free/manifest.json declarative-hex-worlds simulate-scenario \ --scenario \ --script \ --manifest assets/free/manifest.json \ --out /tmp/simple-rpg-simulation.json \ --outInterop /tmp/simple-rpg-interop.json ``` For browser E2E, render the same scenarios rather than creating private renderer-only fixtures. That keeps screenshots tied to the public contract. # Release readiness coverage > Generated ledger of feature + asset coverage at release time. This generated ledger combines the decomposed KayKit guide coverage, manifest coverage, public API treatment, visual artifacts, local reference packs, and package verification gates. Regenerate it with: ```bash pnpm coverage:ledger ``` ## Summary [Section titled “Summary”](#summary) * Status: passed * Guide pages: 19/19 * Guide scenarios: 19 * Guide assets: 404 unique (221 FREE, 183 EXTRA), 1108 page-level occurrences * Public API surfaces: 74 * Public roles: 12 * Visual artifacts: 70 available, 0 missing, 0 skipped * Local references: 4 available, 0 missing, 0 skipped * Release checks: 12 passed, 0 failed, 0 not run, 0 skipped * SimpleRPG API evidence: 74/74 represented, 40 directly executed, 9 active mode(s) ## Manifest Coverage [Section titled “Manifest Coverage”](#manifest-coverage) * Manifest edition: free * Manifest assets: 221 * FREE guide assets in manifest: 221/221 * FREE guide assets missing from manifest: 0 * EXTRA guide assets kept local-only: 183/183 * Manifest validation: 0 error(s), 0 warning(s) ## SimpleRPG Public API Evidence [Section titled “SimpleRPG Public API Evidence”](#simplerpg-public-api-evidence) * Guide-facing public APIs represented: 74/74 * Direct executable helper APIs: 40 * KayKit public treatment records asserted: 404 * Decomposed guide pages asserted: 19 * Missing public APIs: 0 * Stale evidence rows: 0 | Mode | API Count | | --------------------- | --------- | | fixed-gameplay | 30 | | seeded-generation | 10 | | packaged-scenario | 1 | | executable-smoke | 40 | | blueprint-recipe | 4 | | manifest-package | 6 | | compatibility-adapter | 2 | | package-boundary | 3 | | visual-coverage | 26 | ### SimpleRPG Exercise Matrix [Section titled “SimpleRPG Exercise Matrix”](#simplerpg-exercise-matrix) | Public API | Modes | Pages | Assets | Evidence | | ---------------------------------------- | -------------------------------------------------- | ---------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | | `analyzeHexTileRegistry` | executable-smoke | 13 | 0 | Packaged SimpleRPG usage analyzes a runtime tile registry in its executable guide API smoke. | | `coloredUnitAssetId` | executable-smoke | 14, 16, 17, 18 | 112 | Packaged SimpleRPG usage resolves a colored unit asset id in executable smoke. | | `createGameboardBuilder` | fixed-gameplay | 9 | 0 | Fixed SimpleRPG board starts from the public fluent builder. | | `createGameboardLayoutArchetypeRegistry` | executable-smoke, seeded-generation | 6 | 0 | Packaged SimpleRPG usage creates a layout archetype registry in executable smoke. | | `createGameboardLayoutFillRuleFromPiece` | executable-smoke, seeded-generation | 2, 5, 6, 9, 10, 15, 16, 17 | 77 | Packaged SimpleRPG usage creates a piece-backed layout fill rule in executable smoke. | | `createGameboardPlanFromRecipe` | executable-smoke | 2, 7, 8, 9, 10, 11, 12, 13, 16, 17 | 25 | Packaged SimpleRPG usage compiles a recipe into a concrete plan in executable smoke. | | `createGameboardPlanFromTiles` | executable-smoke | 7, 9 | 2 | Packaged SimpleRPG usage rebuilds a plan from explicit tiles in executable smoke. | | `createGameboardRuntimeFromScenario` | packaged-scenario | 9, 18 | 0 | Packaged SimpleRPG usage creates a runtime facade directly from the scenario JSON. | | `createHexagonGameboardGrid` | executable-smoke | 10 | 0 | Packaged SimpleRPG usage creates a Honeycomb hexagon grid in executable smoke. | | `createManifestBundle` | executable-smoke, manifest-package | 12 | 0 | Packaged SimpleRPG usage bundles the FREE manifest in executable smoke. | | `createGameboardBlueprintPlan` | executable-smoke, blueprint-recipe | 6, 10 | 0 | Packaged SimpleRPG usage compiles a blueprint plan in executable smoke. | | `createGameboardBlueprintRecipe` | executable-smoke, blueprint-recipe | 5, 8, 11, 13 | 0 | Packaged SimpleRPG usage compiles a blueprint recipe in executable smoke. | | `createMedievalShowcaseBlueprintRecipe` | executable-smoke, blueprint-recipe | 9, 13 | 0 | Packaged SimpleRPG usage compiles the showcase blueprint recipe in executable smoke. | | `createSeededGameboardPlan` | executable-smoke, seeded-generation | 9 | 0 | Packaged SimpleRPG usage builds a seeded board in executable smoke. | | `declareHexTile` | executable-smoke | 13 | 0 | Packaged SimpleRPG usage declares a tile for registry analysis in executable smoke. | | `executeGameboardInteractionCommand` | fixed-gameplay | 17 | 0 | SimpleRPG quest execution moves, interacts, attacks, and removes enemies through commands. | | `externalAssetSpawnOptions` | executable-smoke, compatibility-adapter | 15 | 0 | Packaged SimpleRPG usage converts compatibility analysis into spawn options in executable smoke. | | `factionBuildingAssetId` | executable-smoke | 2, 15, 16, 17 | 108 | Packaged SimpleRPG usage resolves a faction building asset id in executable smoke. | | `flagAssetId` | executable-smoke | 2, 5 | 4 | Packaged SimpleRPG usage resolves a faction flag asset id in executable smoke. | | `freeManifest` | executable-smoke, manifest-package | 1 | 0 | Packaged SimpleRPG usage reads the FREE manifest in executable smoke. | | `GameboardBuilder.addBridge` | fixed-gameplay, visual-coverage | 2, 7, 9 | 2 | Fixed SimpleRPG board places a bridge beside the harbor approach. | | `GameboardBuilder.addConstructionSite` | fixed-gameplay, visual-coverage | 2, 17 | 7 | Fixed SimpleRPG board places a staged worksite off the golden path. | | `GameboardBuilder.addElevationRamp` | fixed-gameplay, visual-coverage | 8, 10 | 2 | Fixed SimpleRPG board places a ramp against an elevated tile. | | `GameboardBuilder.addFactionBuilding` | fixed-gameplay, visual-coverage | 2, 15, 16, 17 | 108 | Fixed and packaged SimpleRPG boards place faction buildings. | | `GameboardBuilder.addFlag` | fixed-gameplay, visual-coverage | 2, 5 | 4 | Fixed SimpleRPG board places a faction flag and runtime actors use flag assets. | | `GameboardBuilder.addForest` | fixed-gameplay, seeded-generation, visual-coverage | 5, 6, 9, 10 | 12 | Fixed and seeded SimpleRPG boards include forests and tree scatter. | | `GameboardBuilder.addFortification` | fixed-gameplay, visual-coverage | 2, 16, 17 | 11 | Fixed SimpleRPG board places a town wall segment with enclosure metadata. | | `GameboardBuilder.addHarbor` | fixed-gameplay, seeded-generation, visual-coverage | 2, 5, 7, 15 | 25 | Fixed and seeded SimpleRPG boards include a playable harbor/coast relationship. | | `GameboardBuilder.addHill` | fixed-gameplay, seeded-generation, visual-coverage | 5, 6, 9, 10 | 9 | Fixed and seeded SimpleRPG boards include hill terrain and decorations. | | `GameboardBuilder.addMountainStack` | fixed-gameplay, seeded-generation, visual-coverage | 5, 6, 8, 9, 10 | 12 | Fixed, seeded, and packaged SimpleRPG boards place stacked mountains. | | `GameboardBuilder.addNature` | fixed-gameplay, visual-coverage | 5, 6, 9, 10 | 42 | Fixed SimpleRPG board places standalone nature assets. | | `GameboardBuilder.addNeutralStructure` | fixed-gameplay, visual-coverage | 2, 7, 9, 16, 17 | 21 | Fixed SimpleRPG board places a neutral grain building. | | `GameboardBuilder.addProp` | fixed-gameplay, visual-coverage | 2, 5, 15, 16, 17 | 35 | Fixed SimpleRPG quest uses a registered crate prop as a passable actor target. | | `GameboardBuilder.addPropCluster` | fixed-gameplay, visual-coverage | 2, 5, 15, 16, 17 | 31 | Fixed SimpleRPG board places a resource-cache cluster. | | `GameboardBuilder.addRiverPath` | fixed-gameplay, visual-coverage | 4, 7 | 30 | Fixed SimpleRPG board routes a curvy waterless river through the quest road. | | `GameboardBuilder.addRoadPath` | fixed-gameplay, seeded-generation, visual-coverage | 3, 9 | 15 | Fixed, seeded, and packaged SimpleRPG boards use roads for movement routes. | | `GameboardBuilder.addSettlement` | fixed-gameplay, visual-coverage | 2, 16, 17 | 96 | Fixed SimpleRPG board places a settlement home through the settlement alias. | | `GameboardBuilder.addSiegeProjectile` | fixed-gameplay, visual-coverage | 2, 17 | 1 | Fixed SimpleRPG board places a catapult projectile beside the town wall. | | `GameboardBuilder.addTransition` | fixed-gameplay, visual-coverage | 11, 12, 13 | 1 | Fixed SimpleRPG board places a local-only texture transition and marks it EXTRA. | | `GameboardBuilder.addUnit` | fixed-gameplay, visual-coverage | 14, 16, 17, 18 | 137 | Fixed SimpleRPG board places colored and neutral EXTRA unit parts. | | `GameboardBuilder.addUnitPreset` | fixed-gameplay, visual-coverage | 14, 15, 16, 17, 18 | 137 | Fixed SimpleRPG board places a composed soldier preset. | | `GameboardBuilder.scatterDecorations` | fixed-gameplay, seeded-generation, visual-coverage | 5, 6, 9, 10 | 42 | Fixed and seeded SimpleRPG boards scatter decorations deterministically. | | `GameboardBuilder.setCoastEdges` | fixed-gameplay, visual-coverage | 7, 15 | 10 | Fixed SimpleRPG board marks the water edge as coast before adding a harbor. | | `GameboardBuilder.setElevation` | fixed-gameplay, visual-coverage | 8, 10 | 0 | Fixed SimpleRPG board raises a tile and then adds an elevation ramp. | | `GameboardBuilder.setTerrain` | fixed-gameplay, seeded-generation, visual-coverage | 7, 9 | 2 | Fixed SimpleRPG board authors a full water row and seeded generation fills terrain. | | `GameboardBuilder.setTileAsset` | fixed-gameplay, visual-coverage | 7, 8, 9, 10 | 5 | Fixed and packaged SimpleRPG boards override authored tile assets and tags. | | `inspectGameboardBlueprint` | executable-smoke, blueprint-recipe | 6, 11 | 0 | Packaged SimpleRPG usage inspects a blueprint in executable smoke. | | `listCoastGuidePermutations` | executable-smoke | 7, 15 | 10 | Packaged SimpleRPG usage lists coast guide permutations in executable smoke. | | `listKayKitAssetPublicTreatments` | executable-smoke | 1 | 0 | Packaged SimpleRPG usage lists every KayKit asset public treatment in executable smoke. | | `listKayKitGuideScenarios` | executable-smoke | 1, 19 | 0 | Packaged SimpleRPG usage lists every decomposed KayKit guide scenario in executable smoke. | | `listPropClusterAssets` | executable-smoke | 2, 5, 15, 16, 17 | 31 | Packaged SimpleRPG usage resolves prop-cluster assets in executable smoke. | | `listRiverCrossingGuidePermutations` | executable-smoke | 4, 7 | 4 | Packaged SimpleRPG usage lists river crossing permutations in executable smoke. | | `listRiverCurvyGuidePermutations` | executable-smoke | 4, 7 | 2 | Packaged SimpleRPG usage lists curvy river permutations in executable smoke. | | `listRiverGuidePermutations` | executable-smoke | 4, 7 | 24 | Packaged SimpleRPG usage lists river permutations in executable smoke. | | `listRoadGuidePermutations` | executable-smoke | 3, 9 | 15 | Packaged SimpleRPG usage lists road permutations in executable smoke. | | `declarative-hex-worlds manifest` | package-boundary, manifest-package | 12 | 0 | Package smoke validates the CLI manifest and packaged SimpleRPG imports together. | | `neutralUnitAssetId` | executable-smoke | 14, 16, 17, 18 | 25 | Packaged SimpleRPG usage resolves a neutral unit asset id in executable smoke. | | `NOTICE.md` | package-boundary, manifest-package | 19 | 0 | Release/package audits keep KayKit attribution with the SimpleRPG packaged smoke. | | `package.json files` | package-boundary, manifest-package | 19 | 0 | Package audit verifies exports, files, examples, and SimpleRPG package imports. | | `planGameboardInteractionCommand` | fixed-gameplay | 17 | 0 | Fixed SimpleRPG tests plan prop interaction and enemy attack commands. | | `recommendExternalAssetFacing` | executable-smoke, compatibility-adapter | 16 | 0 | Packaged SimpleRPG usage recommends external asset facing in executable smoke. | | `selectCoastVariant` | executable-smoke | 7, 15 | 10 | Packaged SimpleRPG usage selects a coast variant in executable smoke. | | `selectCoastVariantByLabel` | executable-smoke | 7, 15 | 10 | Packaged SimpleRPG usage selects a labeled coast variant in executable smoke. | | `selectManifestAssets` | executable-smoke, manifest-package | 12 | 0 | Packaged SimpleRPG usage selects manifest assets in executable smoke. | | `selectRiverCrossingVariant` | executable-smoke | 4, 7 | 4 | Packaged SimpleRPG usage selects a river crossing variant in executable smoke. | | `selectRiverVariant` | executable-smoke | 4, 7 | 26 | Packaged SimpleRPG usage selects a river variant in executable smoke. | | `selectRiverVariantByLabel` | executable-smoke | 4, 7 | 24 | Packaged SimpleRPG usage selects a labeled river variant in executable smoke. | | `selectRoadVariant` | executable-smoke | 3, 9 | 15 | Packaged SimpleRPG usage selects a road variant in executable smoke. | | `selectRoadVariantByLabel` | executable-smoke | 3, 9 | 15 | Packaged SimpleRPG usage selects a labeled road variant in executable smoke. | | `selectSpawnCoordinates` | executable-smoke | 9 | 0 | Packaged SimpleRPG usage selects raw deterministic spawn coordinates in executable smoke. | | `spawnGameboardActor` | fixed-gameplay | 14, 16, 17, 18 | 137 | Fixed and seeded SimpleRPG fixtures spawn player, NPC, prop, and enemy actors. | | `textureFileName` | executable-smoke | 11, 12 | 0 | Packaged SimpleRPG usage resolves a texture filename in executable smoke. | | `validateGameboardRecipe` | executable-smoke | 11, 12, 13 | 1 | Packaged SimpleRPG usage validates a compiled recipe in executable smoke. | | `validateGameboardRecipeGeneration` | executable-smoke | 13 | 0 | Packaged SimpleRPG usage validates recipe generation config in executable smoke. | ## Gaps [Section titled “Gaps”](#gaps) * None ## Local References [Section titled “Local References”](#local-references) | Status | Reference | Path | Purpose | | --------- | ----------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------ | | available | KayKit Medieval Hexagon FREE | `references/KayKit_Medieval_Hexagon_Pack_1.0_FREE` | FREE source pack for guide extraction, generated assets, and manifest audits. | | available | KayKit Medieval Hexagon EXTRA | `references/KayKit_Medieval_Hexagon_Pack_1.0_EXTRA` | Purchased local-only EXTRA pack for category and guide visual coverage. | | available | Kenney Castle Kit | `references/kenney_castle-kit` | Third-party compatibility fixture for non-hex props, structures, and warnings. | | available | KayKit Adventurers FREE | `references/KayKit_Adventurers_2.0_FREE` | Animated actor fixture for facing, spawn, and SimpleRPG local-asset coverage. | ## Release Checks [Section titled “Release Checks”](#release-checks) | Status | Command | Summary | | ------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | passed | `pnpm lint` | Biome lint over workspace packages, docs scripts, and generated public TypeScript surfaces. | | passed | `pnpm typecheck` | Strict TypeScript validation for runtime, package tests, docs scripts, and generated examples. | | passed | `pnpm build` | Nx package build including tsup ESM chunks, declarations, CLI shebang preservation, and asset copies. | | passed | `pnpm test:ci` | Serialized non-browser release gate: docs contracts, API docs, assets, workspace/workflow audits, CLI smoke, expectations, unit tests, package audit, consumer smoke, and dry-run pack. | | passed | `pnpm test:docs-contract` | Pillar frontmatter/link audit plus README, pillar, and guide SimpleRPG executable coverage contract for 40 guide-facing helper APIs, 404 KayKit public treatment records, and 19 guide pages. | | passed | `pnpm expectations` | Behavior-drift fixtures for seeded generation, SimpleRPG quests, executable guide API smoke, movement, actor targets, patrols, mutations, and final placements. | | passed | `pnpm docs:build` | TypeDoc and VitePress documentation build with public JSDoc and guide-link validation. | | passed | `pnpm test:consumer` | Packed tarball installed into a temporary app, then compiled and executed through public subpaths, examples, and the CLI bin. | | passed | `pnpm test:visual` | FREE, EXTRA, SimpleRPG, Kenney Castle Kit, and KayKit Adventurers browser visual suites with screenshot quality checks. | | passed | `pnpm showcases:promote -- --check` | Curated browser screenshots match committed docs/package showcase copies and pass the shared PNG quality analyzer. | | passed | `pnpm test:workflows` | CI, Release Please, npm OIDC publish, automerge, and Dependabot workflow contract audit. | | passed | `pnpm pack:dry-run` | npm tarball dry run proving publish whitelist, FREE asset inclusion, local reference exclusion, README gallery links, KayKit attribution/NOTICE, and packaged showcase PNG quality. | ## Visual Artifacts [Section titled “Visual Artifacts”](#visual-artifacts) | Status | Source | Artifact | Pages | | --------- | ---------- | ------------------------------------------------------------------------------------------ | -------------- | | available | guide | `docs/assets/kaykit-guide/montage.png` | 1 | | available | guide | `docs/assets/kaykit-guide/pages/page-01.png` | 1 | | available | guide | `docs/assets/kaykit-guide/pages/page-02.png` | 2 | | available | guide | `docs/assets/kaykit-guide/pages/page-03.png` | 3 | | available | guide | `docs/assets/kaykit-guide/pages/page-04.png` | 4 | | available | guide | `docs/assets/kaykit-guide/pages/page-05.png` | 5 | | available | guide | `docs/assets/kaykit-guide/pages/page-06.png` | 6 | | available | guide | `docs/assets/kaykit-guide/pages/page-07.png` | 7 | | available | guide | `docs/assets/kaykit-guide/pages/page-08.png` | 8 | | available | guide | `docs/assets/kaykit-guide/pages/page-09.png` | 9 | | available | guide | `docs/assets/kaykit-guide/pages/page-10.png` | 10 | | available | guide | `docs/assets/kaykit-guide/pages/page-11.png` | 11 | | available | guide | `docs/assets/kaykit-guide/pages/page-12.png` | 12 | | available | guide | `docs/assets/kaykit-guide/pages/page-13.png` | 13 | | available | guide | `docs/assets/kaykit-guide/pages/page-14.png` | 14 | | available | guide | `docs/assets/kaykit-guide/pages/page-15.png` | 15 | | available | guide | `docs/assets/kaykit-guide/pages/page-16.png` | 16 | | available | guide | `docs/assets/kaykit-guide/pages/page-17.png` | 17 | | available | guide | `docs/assets/kaykit-guide/pages/page-18.png` | 18 | | available | guide | `docs/assets/kaykit-guide/pages/page-19.png` | 19 | | available | showcase | `docs/assets/showcases/extra-blueprint-biome-transition-showcase.png` | - | | available | showcase | `docs/assets/showcases/extra-harbor-gameboard.png` | - | | available | showcase | `docs/assets/showcases/free-blueprint-builder-showcase.png` | - | | available | showcase | `docs/assets/showcases/free-guide-coasts-all-labels-rotations-water-waterless.png` | - | | available | showcase | `docs/assets/showcases/free-guide-rivers-all-labels-rotations-water-waterless.png` | - | | available | showcase | `docs/assets/showcases/free-guide-roads-all-labels-rotations.png` | - | | available | showcase | `docs/assets/showcases/free-guide-scenarios-by-extracted-page.png` | - | | available | showcase | `docs/assets/showcases/simple-rpg-fixed-completed.png` | - | | available | showcase | `docs/assets/showcases/simple-rpg-local-third-party-assets.png` | - | | available | showcase | `docs/assets/showcases/simple-rpg-seeded-completed.png` | - | | available | guide | `NOTICE.md` | 19 | | available | showcase | `docs/showcases/extra-blueprint-biome-transition-showcase.png` | - | | available | showcase | `docs/showcases/extra-harbor-gameboard.png` | - | | available | showcase | `docs/showcases/free-blueprint-builder-showcase.png` | - | | available | showcase | `docs/showcases/free-guide-coasts-all-labels-rotations-water-waterless.png` | - | | available | showcase | `docs/showcases/free-guide-rivers-all-labels-rotations-water-waterless.png` | - | | available | showcase | `docs/showcases/free-guide-roads-all-labels-rotations.png` | - | | available | showcase | `docs/showcases/free-guide-scenarios-by-extracted-page.png` | - | | available | showcase | `docs/showcases/simple-rpg-fixed-completed.png` | - | | available | showcase | `docs/showcases/simple-rpg-local-third-party-assets.png` | - | | available | showcase | `docs/showcases/simple-rpg-seeded-completed.png` | - | | available | guide | `tests/browser/__screenshots__/extra-blueprint-biome-transition-showcase.png` | 9, 11, 13 | | available | screenshot | `tests/browser/__screenshots__/extra-guide-assets-by-public-role.png` | - | | available | screenshot | `tests/browser/__screenshots__/extra-guide-scenarios-pages-02-15.png` | - | | available | screenshot | `tests/browser/__screenshots__/extra-guide-scenarios-pages-16-18.png` | - | | available | guide | `tests/browser/__screenshots__/extra-harbor-gameboard.png` | 7, 15 | | available | guide | `tests/browser/__screenshots__/extra-local-all-buildings-factions-neutral-harbors.png` | 2, 15, 17 | | available | guide | `tests/browser/__screenshots__/extra-local-all-decoration-nature-props.png` | 5, 16 | | available | guide | `tests/browser/__screenshots__/extra-local-all-tiles-guide-and-transitions.png` | 11, 12, 13 | | available | guide | `tests/browser/__screenshots__/extra-local-all-units-full-accent-neutral-siege.png` | 14, 16, 17, 18 | | available | guide | `tests/browser/__screenshots__/extra-seasonal-textures.png` | 11, 12, 13 | | available | guide | `tests/browser/__screenshots__/free-blueprint-builder-showcase.png` | 8, 9 | | available | screenshot | `tests/browser/__screenshots__/free-catalog.png` | - | | available | guide | `tests/browser/__screenshots__/free-gameboard-recipe.png` | 8, 9 | | available | guide | `tests/browser/__screenshots__/free-generated-piece-recipe.png` | 6 | | available | screenshot | `tests/browser/__screenshots__/free-guide-assets-by-public-role.png` | - | | available | guide | `tests/browser/__screenshots__/free-guide-coasts-all-labels-rotations-water-waterless.png` | 7 | | available | guide | `tests/browser/__screenshots__/free-guide-page-nature-stacks-buildings-props.png` | 2, 5, 6, 8, 10 | | available | guide | `tests/browser/__screenshots__/free-guide-river-curvy-crossings-all-modes.png` | 4 | | available | guide | `tests/browser/__screenshots__/free-guide-rivers-all-labels-rotations-water-waterless.png` | 4, 7 | | available | guide | `tests/browser/__screenshots__/free-guide-roads-all-labels-rotations.png` | 3 | | available | screenshot | `tests/browser/__screenshots__/free-guide-scenarios-by-extracted-page.png` | - | | available | screenshot | `tests/browser/__screenshots__/free-guide-source-pages.png` | - | | available | guide | `tests/browser/__screenshots__/free-seeded-gameboard.png` | 9 | | available | guide | `tests/browser/__screenshots__/free-seeded-hex-gameboard.png` | 10 | | available | guide | `tests/browser/__screenshots__/simple-rpg-fixed-completed.png` | 9 | | available | guide | `tests/browser/__screenshots__/simple-rpg-local-third-party-assets.png` | 14 | | available | screenshot | `tests/browser/__screenshots__/simple-rpg-packaged-scenario.png` | - | | available | guide | `tests/browser/__screenshots__/simple-rpg-seeded-completed.png` | 18 | | available | screenshot | `tests/browser/__screenshots__/simple-rpg-simulation-report.png` | - | ## Guide Pages [Section titled “Guide Pages”](#guide-pages) | Page | Scenario | Edition | Assets | APIs | Docs | Visuals | Source Image | | ---- | -------------------------------------- | --------- | ------ | ---- | ---- | ------- | ------------------------------------------------------ | | 1 | `page-01-overview-and-license` | reference | 0 | 3 | 3 | 2 | available `docs/assets/kaykit-guide/pages/page-01.png` | | 2 | `page-02-buildings-props-and-factions` | mixed | 164 | 16 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-02.png` | | 3 | `page-03-road-variations` | free | 15 | 4 | 2 | 1 | available `docs/assets/kaykit-guide/pages/page-03.png` | | 4 | `page-04-river-variations` | free | 30 | 7 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-04.png` | | 5 | `page-05-nature-contents` | free | 77 | 13 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-05.png` | | 6 | `page-06-nature-usage` | free | 42 | 9 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-06.png` | | 7 | `page-07-water-usage` | free | 44 | 18 | 2 | 3 | available `docs/assets/kaykit-guide/pages/page-07.png` | | 8 | `page-08-taller-hex-tiles` | free | 3 | 6 | 2 | 3 | available `docs/assets/kaykit-guide/pages/page-08.png` | | 9 | `page-09-world-design-example` | free | 61 | 21 | 2 | 5 | available `docs/assets/kaykit-guide/pages/page-09.png` | | 10 | `page-10-floating-islands` | free | 45 | 12 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-10.png` | | 11 | `page-11-biomes` | extra | 1 | 6 | 2 | 3 | available `docs/assets/kaykit-guide/pages/page-11.png` | | 12 | `page-12-alternate-textures` | extra | 1 | 7 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-12.png` | | 13 | `page-13-transition-tiles` | extra | 1 | 8 | 2 | 3 | available `docs/assets/kaykit-guide/pages/page-13.png` | | 14 | `page-14-units` | extra | 137 | 5 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-14.png` | | 15 | `page-15-shipyard-harbors` | mixed | 25 | 13 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-15.png` | | 16 | `page-16-stables-and-horses` | extra | 155 | 16 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-16.png` | | 17 | `page-17-workshop-and-siege` | extra | 170 | 19 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-17.png` | | 18 | `page-18-unit-combinations` | extra | 137 | 6 | 2 | 2 | available `docs/assets/kaykit-guide/pages/page-18.png` | | 19 | `page-19-supporters-and-attribution` | reference | 0 | 3 | 3 | 2 | available `docs/assets/kaykit-guide/pages/page-19.png` | ## Final Commands [Section titled “Final Commands”](#final-commands) * `pnpm lint` * `pnpm typecheck` * `pnpm build` * `pnpm test:ci` * `pnpm test:docs-contract` * `pnpm expectations` * `pnpm docs:build` * `pnpm test:consumer` * `pnpm test:visual` * `pnpm showcases:promote -- --check` * `pnpm test:workflows` * `pnpm pack:dry-run` # Rendering, assets, and external packs > How the library ships assets, bootstraps FREE, and supports EXTRA + third-party packs. The published package ships the FREE KayKit assets and manifest. Purchased EXTRA assets and third-party fixtures stay local-only unless an application chooses to copy its own generated manifest and source URL map into its codebase. ## FREE Assets [Section titled “FREE Assets”](#free-assets) Use `freeManifest` for packaged assets. It includes asset ids, categories, subcategories, factions, texture sets, bounds, material slots, local package paths, and KayKit license metadata. ```ts import { freeManifest } from 'declarative-hex-worlds/manifest/free'; import { createManifestBundle, getManifestAsset, resolveManifestAssetUrl, } from 'declarative-hex-worlds/manifest/schema'; const bundle = createManifestBundle([freeManifest]); const asset = getManifestAsset(bundle, 'hex_river_A'); const url = asset ? resolveManifestAssetUrl(asset, { baseUrl: '/node_modules/declarative-hex-worlds/assets/free', }) : undefined; ``` Browser bundles often rewrite package asset URLs. Keep that mapping at the app boundary instead of baking local absolute paths into manifests. Use `describeKayKitAssetTreatment(assetId)` from `./catalog` when an editor or tool needs to explain what an asset is for. Treatment records connect each FREE/EXTRA asset id to a role, placement kind/layer, extracted guide image, and the public builder or selector API that exercises it. This prevents an asset from being merely present in a manifest without a gameboard-facing path. Use `listKayKitGuideScenarios()` when the tool needs the page-level source contract: each extracted guide page lists its source PNG, covered assets, public APIs, docs, and visual artifacts. Use `listKayKitGuideScenarioTreatments(id)` or `describeKayKitGuideScenarioCoverage(id)` to join a page back to the asset treatment records, `listKayKitGuideAssetCoverages()` when a tool needs to start from an exact asset id, `listKayKitGuideRoleCoverages()` when it needs to start from a gameplay role, `listKayKitGuidePublicApiCoverages()` when it needs to start from a public API surface, `listKayKitGuideScenarioAssetRenderRequests()` when it needs URL-resolved render queues, `listKayKitGuideScenarioAssetRenderGroups()` when it needs guide-page contact-sheet groups, `guide-render-requests` when it needs the same queue from the CLI, and `summarizeKayKitGuideCoverage()` when a build tool or editor needs stable counts for pages, editions, roles, unique assets, repeated page-level asset occurrences, docs, and visual artifacts. ## EXTRA Assets [Section titled “EXTRA Assets”](#extra-assets) EXTRA assets are supported through local ingest. They must not be published by this package. Apps that own the assets can generate an app-local manifest and combine it with the packaged FREE manifest: ```ts import { freeManifest } from 'declarative-hex-worlds/manifest/free'; import { createManifestBundle } from 'declarative-hex-worlds/manifest/schema'; import extraManifest from './generated/kaykit-extra-manifest.json'; const bundle = createManifestBundle([freeManifest, extraManifest]); ``` The static `import extraManifest from '...json'` shown above assumes the generated manifest lives in your **source tree**. If you wrote it under a Vite `public/` root (the natural outcome of `extract --out public/assets/...`), Vite refuses to import it (“Assets in public directory cannot be imported from JavaScript”) — `fetch()` it at runtime instead and pass the parsed JSON to `createManifestBundle`. Every placement that points at an EXTRA asset should keep `requiresExtra: true`. That lets validation and renderers distinguish missing local content from package bugs. ## Three.js Bridge [Section titled “Three.js Bridge”](#threejs-bridge) The `./three` subpath is a thin bridge around public board state. It does not own the game loop. Use it to: * resolve a placement to a loadable URL. * load GLTFs with an app-provided loader. * apply position, rotation, scale, and placement metadata. * keep a scene in sync with added, changed, and removed placements. * tag objects so raycasts can resolve placement ids, tile keys, actor ids, and source asset ids. * attach optional animation clips for rigged units. ```ts import { createGameboardPlacementAssetUrlResolver, gameboardInteractionTargetForObject, syncGameboardPlacementObjects, } from 'declarative-hex-worlds/three'; const sourceAssetUrls = runtime.createScenarioPieceSourceUrlMap({ sourceRoots: { 'Kenney Castle Kit': '/assets/kenney/castle-kit' }, }); const resolveAssetUrl = createGameboardPlacementAssetUrlResolver({ catalog: manifestBundle, assetUrls: sourceAssetUrls, baseUrl: '/assets/kaykit/free', }); const previewUrl = resolveAssetUrl(runtime.plan().placements[0]); const sync = await syncGameboardPlacementObjects(runtime.plan().placements, { parent: scene, loader, catalog: manifestBundle, assetUrls: sourceAssetUrls, baseUrl: '/assets/kaykit/free', }); const target = gameboardInteractionTargetForObject(raycasterHit.object); ``` Animation loading is explicit because different games manage mixers and clips differently. The bridge exposes clip metadata so an app can connect the loaded clips to its own animation system. ### Performance at scale [Section titled “Performance at scale”](#performance-at-scale) `syncGameboardPlacementObjects` (and `loadGameboardPlacementObject`) dedupe GLTF loads by URL automatically: placements that resolve to the same model or animation URL trigger exactly one `loader.loadAsync` call per sync generation, no matter how many placements share it. A failed load is evicted from the cache so the next sync retries instead of caching the failure. This is on by default; pass `cacheLoads: false` if a caller needs every placement to issue its own load. Deduping loads does not change the draw-call story: each placement still gets its own cloned `Object3D` subtree. A single-mesh asset is one draw call per placement; multi-mesh or multi-material GLTFs (rigged units especially) issue one draw call per mesh/material group. For a 98-placement showcase board of mostly single-mesh tiles this measures as 98 draw calls (`renderer.info.render.calls`) — fine at that scale. Boards with hundreds to thousands of tiles should batch per-asset-id with `THREE.InstancedMesh` on top of the placement snapshot the bridge already produces; the bridge intentionally doesn’t own that batching decision. ## External Compatibility [Section titled “External Compatibility”](#external-compatibility) Run compatibility checks on local GLB/GLTF files before registering them as pieces. A mesh that is not a KayKit-compatible hex tile can still be useful as a prop, landmark, building, tree, scatter item, or unit. ```bash declarative-hex-worlds compatibility \ --asset "references/kenney_castle-kit/Models/GLB format/tower-hexagon-base.glb" \ --intendedRole tile \ --sourcePack "Kenney Castle Kit" \ --modelForward +z \ --boardForwardEdge 1 declarative-hex-worlds pieces-from-assets \ --assets "references/kenney_castle-kit/Models/GLB format" \ --sourcePack "Kenney Castle Kit" \ --intendedRole tile \ --assetIdPrefix kenney \ --pieceIdPrefix kenney-castle \ --tags castle \ --pieceOverrides docs/examples/local-piece-overrides.kenney-castle.json \ --includeReports \ --out /tmp/kenney-pieces.json ``` The batch output omits absolute paths by default. Renderer URL maps should be generated from local source roots at app build time: ```bash declarative-hex-worlds pieces \ --pieces /tmp/kenney-pieces.json \ --emitSourceUrls \ --pieceSourceRoots docs/examples/local-piece-source-roots.example.json \ --json ``` ## Placement Criteria [Section titled “Placement Criteria”](#placement-criteria) Use explicit criteria for non-KayKit assets: * towers and large structures usually need multi-tile footprints and occupancy reservations. * trees and loose props should use scatter slots, `maxPerTile`, and a soft slot group. * units should declare facing and animation expectations. * circular towers, square walls, and other non-hex meshes should be props or landmarks unless the compatibility report proves a matching tile footprint. * ships, docks, ports, and harbor props should target coast tiles adjacent to water. This placement metadata is what lets seeded boards stay plausible while still accepting assets from another designer. ## Browser Verification [Section titled “Browser Verification”](#browser-verification) Rendering tests should exercise public fixtures: ```bash pnpm test:browser:free pnpm test:browser:extra pnpm test:e2e:local-assets pnpm test:visual ``` Screenshots are deterministic artifacts. The test suite checks image size, variance, and flat-output failures after Chromium captures the PNGs. When adding new visible behavior, prefer a scenario or recipe fixture that can also be used by CLI validation and headless simulation tests. The FREE browser suite also captures `free-guide-source-pages.png` and `free-guide-scenarios-by-extracted-page.png` so visual review can start from the decomposed KayKit guide pages and then inspect every FREE treatment associated with those pages. The EXTRA browser suite follows the same page contract through `extra-guide-scenarios-pages-02-15.png` and `extra-guide-scenarios-pages-16-18.png`, covering mixed and EXTRA page-level asset occurrences for buildings, transitions, units, harbors, stables, workshops, siege pieces, and unit combinations. # Runtime integration > Wire the runtime facade into a React + Three.js app. Use the runtime facade as the application boundary for a playable board. The lower-level modules remain public, but a game usually wants one object that owns the Koota world, exposes safe mutations, projects render plans, runs systems, and emits snapshots for UI or another ECS. ## Runtime Ownership [Section titled “Runtime Ownership”](#runtime-ownership) A scene should create exactly one runtime for one board instance: * Use `createGameboardRuntime(plan)` when the app already has a validated `GameboardPlan`. * Use `createGameboardRuntimeFromRecipe(recipe)` when the app loads saved board intent and still needs recipe-local piece registries. * Use `createGameboardRuntimeFromScenario(scenario)` when the app loads actors, movement agents, patrols, quests, spawn groups, or SimpleRPG-style fixtures. The runtime keeps `runtime.world` public so advanced users can run raw Koota queries or mount the world in React. Prefer the facade methods first when code is about gameplay, editor previews, save data, or integration tests; those methods project live board state before navigation, layout, and interop reads. ## Play Loop [Section titled “Play Loop”](#play-loop) The common loop is deterministic: 1. Validate the recipe, scenario, or plan before startup. 2. Create the runtime. 3. Spawn or register any transient scene actors and props. 4. Convert pointer/raycast targets into commands. 5. Dispatch commands and run system ticks. 6. Render `runtime.plan()` or `runtime.snapshot().plan`. 7. Mirror `runtime.createInteropSnapshot()` into another ECS if needed. ```ts import { createGameboardRuntimeFromScenario, } from 'declarative-hex-worlds/runtime'; import { createGameboardInteractionHandlerPreset, } from 'declarative-hex-worlds/commands'; const runtime = createGameboardRuntimeFromScenario(scenario); const handlers = createGameboardInteractionHandlerPreset('default-rpg'); function frame(step: number, clickedTarget?: string) { if (clickedTarget) { runtime.dispatchCommand(clickedTarget, { sourceActor: 'player', handlers, systems: false, }); } const tick = runtime.tick({ patrols: true, movement: { steps: 4 }, quests: { step }, }); renderBoard(runtime.plan()); updateHud(runtime.readActors(), runtime.readQuests(), tick.eventRecords); } ``` ## Runtime Mutations [Section titled “Runtime Mutations”](#runtime-mutations) Runtime placements are for gameplay state that changes after startup: units, construction previews, dropped props, markers, blockers, and temporary effects. Use occupancy preflight when a placement can block movement or reserve a footprint. ```ts const preview = runtime.inspectPlacementOccupancy({ at: '2,1', kind: 'structure', footprint: { kind: 'adjacent', edges: [0, 1], includeCenter: true }, }); if (preview.canOccupy) { const tower = runtime.spawnPlacement({ id: 'watchtower-01', at: '2,1', assetId: 'kenney:round-tower', kind: 'structure', occupancyGuard: true, }); runtime.registerActor(tower, { actorId: 'watchtower-01', actorKind: 'prop', blocking: true, tags: ['player-built'], }); } ``` Use `runtime.readPlacements()` and `runtime.readPlacementOccupancy()` for whole board save files, editor panels, and bridge code. Use `runtime.readPlacementsForTile(tileKey)` and `runtime.readPlacementOccupancyForTile(tileKey)` when a hover panel, collision probe, or host ECS sync only needs one hex. Use `runtime.readActorsForTile(tileKey)` when that same one-hex read needs actor kinds, teams, hostility, tags, or interaction flags rather than raw placement records. Use `runtime.removePlacement(id)` for cleanup; it removes the placement entity and its placement relations so future navigation and occupancy reads use the current world. Use `runtime.summarizePlan()` when a tool needs aggregate coverage from the current live board instead of raw placement arrays. It returns counts by terrain, texture set, elevation, tile tag, placement kind/layer, semantic feature, asset id, and local-only asset usage. The same pure helper is available as `summarizeGameboardPlan(plan)` for build-time recipes, browser screenshot manifests, editor sidebars, and ECS bridge preflight checks. ## Actor And Quest Reads [Section titled “Actor And Quest Reads”](#actor-and-quest-reads) Actors are placement-backed. That keeps render transforms, collision, target selection, and quest objectives tied to the same tile occupancy model. ```ts const nearbyEnemies = runtime.selectActors({ sourceActor: 'player', hostileToSource: true, radius: 4, sort: 'distance', }); const targetPlan = runtime.inspectActorTargets({ sourceActor: 'player', hostileToSource: true, maxPathCost: 6, }); if (targetPlan.nearestTarget) { runtime.interactActorTarget( { sourceActor: 'player', targetActorId: targetPlan.nearestTarget.actor.actorId }, { handlers, systems: { movement: false, quests: true } } ); } for (const quest of runtime.advanceAllQuests({ step: frameNumber })) { if (quest.quest.status === 'completed') { unlockReward(quest.quest.questId); } } ``` ## Seeded Scene Assembly [Section titled “Seeded Scene Assembly”](#seeded-scene-assembly) Random board generation should still be inspectable before it mutates the live world. Analyze fills first, then spawn the same rules after the app accepts the diagnostics. ```ts const fill = { seed: 'campaign-01:forest', rules: [{ id: 'trees', archetype: 'tree', assetId: 'tree_single_A', fill: 0.18 }], } as const; const analysis = runtime.analyzeLayoutFill(fill); if (analysis.errors.length === 0) { runtime.spawnLayoutFill(fill); } const spawnPlan = runtime.planSpawnGroups({ seed: 'campaign-01:encounters', groups: [ { id: 'player', count: 1, tileTags: ['player-spawn'] }, { id: 'enemy', count: 3, minDistanceFromGroups: 4, pathToGroups: ['player'] }, ], }); ``` Keep seed namespaces stable. Board terrain, layout fills, piece fills, spawn groups, patrol plans, and combat randomness should not all consume one stream if the game needs reproducible maps across content edits. ## React Runtime Boundary [Section titled “React Runtime Boundary”](#react-runtime-boundary) React components can mount a runtime created outside React with `GameboardRuntimeProvider`, or load content directly with `GameboardPlanProvider`, `GameboardRecipeProvider`, and `GameboardScenarioProvider`. Use hook families by intent: | Intent | Hooks | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Runtime and snapshots | `useGameboardRuntime`, `useGameboardRuntimeSnapshot`, `useProjectedGameboardPlan` | | Mutable gameplay actions | `useGameboardActions`, `useGameboardActorActions`, `useGameboardCommandActions`, `useGameboardMovementActions`, `useGameboardPatrolActions`, `useGameboardQuestActions`, `useGameboardSystemActions` | | Serializable reads | `useGameboardPlacementSnapshots`, `useGameboardActorSnapshots`, `useGameboardQuestSnapshots`, `useGameboardPlacementOccupancy` | | Tile-local UI and collision probes | `useGameboardTileInspection`, `useGameboardNeighborhoodInspection`, `usePlacementOccupancyForTile`, `useGameboardActorsForTile`, `usePlacementEntitiesForTile`, `useOriginPlacementEntitiesForTile` | | Actor commands and target overlays | `useGameboardActorSelection`, `useGameboardActorTargets`, `useGameboardActorTargetCommand`, `useGameboardInteractionTarget`, `useGameboardInteractionCommand`, `useGameboardInteractionCommandPreview` | | Navigation and spawned NPC setup | `useGameboardOccupancyIndex`, `useGameboardNavigation`, `useGameboardSpawnLocations`, `useGameboardPatrolRoute`, `useGameboardPatrolRoutes` | | Build cursors and generated content | `useGameboardLayoutSiteInspection`, `useGameboardLayoutFillAnalysis`, `useGameboardLayoutPlacements`, `useGameboardPieceRegistryAnalysis`, `useGameboardPieceSelection`, `useGameboardPiecePlacementInspection`, `useGameboardPieceFillInspection`, `useGameboardPieceSourceUrlMap` | | Raw Koota trait reads | entity queries such as `useGameboardTileEntities`, plus trait hooks such as `useTileCoordinates`, `usePlacementState`, `useGameboardActor`, and `useGameboardQuest` | | Live rule checks | `useGameboardPlacementOccupancyInspection`, `useCanOccupyGameboardPlacement`, `useGameboardRuleViolations` | Those hooks subscribe to trait and relation value changes, so moving an existing entity in place rerenders the UI even when query membership does not change. ## External ECS Bridge [Section titled “External ECS Bridge”](#external-ecs-bridge) Use interop snapshots when a host game already owns the simulation ECS but wants KayKit-aware board rules. ```ts const snapshot = runtime.createInteropSnapshot({ includeActors: true, includeQuests: true, }); externalEcs.apply(snapshot.entities, snapshot.relations); ``` For callback-style stores, `runtime.mountInterop(adapter)` sends the same snapshot through an adapter and returns the host-entity mapping. ## Integration Tests [Section titled “Integration Tests”](#integration-tests) Integration tests should use the public runtime the same way a game does: * Load a fixed scenario and assert the golden path. * Load a seeded scenario with stable seed namespaces and assert the chosen board, actors, quests, collisions, and routes. * Dispatch commands through `runtime.dispatchCommand` or `runtime.interactActorTarget` instead of calling lower-level movement helpers directly. * Run `runtime.tick` for patrols, movement, commands, and quests. * Capture browser screenshots from `runtime.plan()` and assert the generated files are nonblank. The package SimpleRPG browser tests follow this pattern for fixed and seeded boards, local-only external pieces, actor targeting, collision, and quest completion. # Testing > The unit + browser + e2e test trinity, coverage gates, SimpleRPG-as-driver, perf benches. ## The test trinity [Section titled “The test trinity”](#the-test-trinity) Three vitest harnesses + one perf harness. Coverage from all of them feeds into a merged report (PRD R6). | Harness | Config | Includes | Cadence | | -------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | | **Unit** | `vitest.config.ts` | `src/**/__tests__/*.test.ts`, `tests/unit/**/*.test.ts`, `tests/integration/**/*.test.ts` | every PR (`pnpm test`) | | **Browser FREE** | `vitest.browser.free.config.ts` | `tests/browser/{free-visual,simple-rpg-visual,react-bindings}.test.ts`, `tests/browser/feature-gallery.spec.ts`, harness smoke | required merged coverage job on every PR; full screenshot command is local proof | | **Browser EXTRA** | `vitest.browser.extra.config.ts` | `tests/browser/extra-visual.test.ts` | local-only with `HEX_WORLDS_ENABLE_EXTRA=1` | | **E2E local-assets** | `vitest.browser.local-assets.config.ts` | `tests/e2e/local-assets/**/*.test.ts` | local-only with `HEX_WORLDS_ENABLE_LOCAL_ASSETS=1` | | **SimpleRPG e2e (GitHub)** | `vitest.simple-rpg-e2e.config.ts` | `tests/e2e/simple-rpg-ci.test.ts` | scheduled CI with `HEX_WORLDS_E2E_GITHUB=1` | | **SimpleRPG e2e (local)** | `vitest.simple-rpg-e2e.config.ts` | `tests/e2e/simple-rpg-local-extra.test.ts` | local with `HEX_WORLDS_LOCAL_REFERENCES=1` | | **Perf bench** | n/a — direct vitest bench | `tests/perf/*.bench.ts` | local-only, non-blocking | ## Coverage gates [Section titled “Coverage gates”](#coverage-gates) `vitest.coverage.shared.ts` exports `COVERAGE_THRESHOLDS` at the current floor. PRD A8 ratchets these toward 100/100/100/100 via Epic E0-E10; each commit that closes a coverage gap raises the floor in the same commit. CI’s required `Coverage` job bootstraps FREE models, collects unit coverage, collects browser-free coverage, then runs `pnpm coverage:merge:enforce`; regressions block merge. To merge harness reports locally: ```bash HEX_WORLDS_COVERAGE=1 pnpm coverage:all open coverage/merged/lcov-report/index.html ``` ## SimpleRPG: the coverage driver [Section titled “SimpleRPG: the coverage driver”](#simplerpg-the-coverage-driver) `tests/integration/simple-rpg/simple-rpg.ts` is a 1,005-line driver that exercises 80+ public APIs synchronously. Its purpose isn’t gameplay — it’s coverage. Read the [SimpleRPG README](https://github.com/jbcom/declarative-hex-worlds/tree/main/tests/simple-rpg/README.md) for the API matrix. Three entry-point functions matter: * `runSimpleRpgUsageExample()` — full scenario → simulation → snapshot path; returns a `SimpleRpgUsageSummary` with every metric the coverage ledger needs. * `summarizeSimpleRpgGuidePublicApiExercises()` — pure-data coverage map; safe to call repeatedly (no koota worlds). * `runSimpleRpgExecutableGuideApiSmoke()` — executable smoke of every guide-page helper API. The CLI’s `coverage` subcommand consumes these to emit `docs/release-readiness.json` + Markdown ledgers. ## Perf benches [Section titled “Perf benches”](#perf-benches) `tests/perf/warm-start.bench.ts` (PRD A3b) tracks the cost of blueprint → board → koota runtime → facade snapshot. Run: ```bash pnpm bench:warm-start ``` Baseline as of 2026-05-26: \~27 Hz / 37 ms mean. Add more benches to `tests/perf/` as PRD B/D-series perf work lands. The bench harness is opt-in (not in default `pnpm test`). ## Visual regression [Section titled “Visual regression”](#visual-regression) `tests/browser/__screenshots__/` holds committed PNG snapshots that vitest-browser compares against every render. Drift fails the build until either the diff is accepted (new snapshot committed) or fixed. The screenshot assertion script is `tests/scripts/assert-screenshots.ts`; local visual commands call it via `pnpm test:screenshots:free` + `:extra` + `:local-assets`. ## What CI actually runs [Section titled “What CI actually runs”](#what-ci-actually-runs) See `.github/workflows/ci.yml`. The chain (post-PRD A9 install-once): 1. `install` job — `pnpm install --frozen-lockfile` once, uploads `node_modules.tar.zst` artifact. 2. `check` matrix — `lint`, `typecheck`, `build`, `test` (each downloads + restores the artifact). 3. `coverage` — bootstraps FREE models, collects unit + browser-free coverage, then enforces the merged ratchet. 4. `docs-site` — Astro Starlight build with generated CLI reference. 5. `dependency-review` — fail-on-severity: high. 6. `semgrep` — OWASP Top 10 + Node.js SAST. Local proof uses `pnpm verify` for the fast source gates and `pnpm coverage:all:enforce` for the CI-shaped merged coverage gate. # Tilesets > Declare a sprite-sheet tileset, resolve biomes to cells, and render seamless painterly hex terrain in three.js or canvas-2D. A **tileset** renders each hex as a cell of a sprite sheet — a painterly 2D/2.5D alternative to the 3D GLTF model path. It’s how you get continuous, illustrated terrain (grass, coast, mountains) instead of discrete meshes. The same declared tileset drives both the three.js and canvas-2D bindings. ## The manifest [Section titled “The manifest”](#the-manifest) A tileset is described by a `TilesetManifest`: named **sheets** (each a PNG with a cell grid) and a **biome → sheet** map. ```ts import { parseTilesetManifest } from 'declarative-hex-worlds/asset-source'; const manifest = parseTilesetManifest({ schemaVersion: '1', kind: 'tileset', sheets: { grassland: { url: '/assets/tilesets/grassland.png', // The sheet is a cols×rows grid of cellWidth×cellHeight px cells. grid: { cols: 5, rows: 10, cellWidth: 96, cellHeight: 83 }, role: 'fill', }, coast: { url: '/assets/tilesets/coast.png', grid: { cols: 4, rows: 4, cellWidth: 64, cellHeight: 64 }, role: 'transition', // A transition sheet maps an edge mask → a specific cell index. edgeCells: { '5': 3, '10': 7 }, }, }, biomes: { meadow: { sheet: 'grassland', select: 'hash' }, // pick a fill cell by tile-key hash }, }); ``` * **`role: 'fill'`** — cells are interchangeable variations of one biome; `select` is `'hash'` (stable per-tile pick) or `'first'`. * **`role: 'transition'`** — cells are positional; an edge mask selects a specific cell via `edgeCells` (see [Transitions](#transitions)). `parseTilesetManifest` throws a precise Zod error on a malformed manifest, so bad data fails at the boundary, not deep in rendering. ## The source [Section titled “The source”](#the-source) `createTilesetSource` turns the manifest into an `AssetSource` — the neutral resolution seam a binding dispatches on: ```ts import { createTilesetSource } from 'declarative-hex-worlds/asset-source'; const source = createTilesetSource({ manifest }); // resolve a placement (its metadata.biome or assetId names the biome) → a // { type: 'tileset-cell', shape: 'quad', cell, hex, sheetUrl } render request. ``` Options: * **`shape`** — `'quad'` (default) draws the full cell as a rectangle; painterly hex atlases paint each cell as a hex with transparent corners, so a full quad lets neighbours’ opaque bodies fill each other’s corners into **seamless** terrain. `'hex'` clips to a hexagon (only for cells that are opaque edge-to-edge). * **`hex`** — override the world footprint of a cell (see [Seamless tessellation](#seamless-tessellation)). ## Rendering with `` (three.js) [Section titled “Rendering with \ (three.js)”](#rendering-with-hexworld-threejs) The declarative element layer registers the source and renders the cells: ```tsx import { Canvas } from '@react-three/fiber'; import { parseTilesetManifest, tilesetHexGeometry } from 'declarative-hex-worlds/asset-source'; import { HexWorld, Tileset, Tile, GameboardObjects } from 'declarative-hex-worlds/react-elements'; function Board({ plan, textureLoader }) { const geometry = tilesetHexGeometry(manifest); // see below return ( {/* …one per hex, or let a projected plan carry the biomes… */} ); } ``` * **``** registers the source. Pass `hex` to override the cell footprint. * **``** declares a hex’s biome. The biome is carried on the placement’s `metadata.biome`, which survives the tile→runtime→projection round-trip (a biome baked only into a *plan* placement’s `assetId` is re-derived away — declare it via `` or the tile’s terrain). * **``** is the render bridge: it reconciles each resolved cell into a textured-hex mesh every frame. It is required — `` alone registers the source but draws nothing. * **No GLTF `loader`** is needed for a tileset-only board — only a `textureLoader` (a three `TextureLoader` wrapped to report the sheet’s pixel size; see `GameboardSheetTextureLoader`). ## Rendering with canvas-2D [Section titled “Rendering with canvas-2D”](#rendering-with-canvas-2d) The `/canvas2d` binding draws the same declared source with zero renderer deps — `drawImage` blits each cell onto a 2D context: ```ts import { syncCanvas2dPlacements } from 'declarative-hex-worlds/canvas2d'; syncCanvas2dPlacements(ctx, plan.placements, { source, sheets }); ``` Its existence is the proof the resolution seam is substrate-agnostic: one declared tileset, two renderers. ## Seamless tessellation [Section titled “Seamless tessellation”](#seamless-tessellation) Painterly hex atlases bake a **vertically-foreshortened** (isometric) hex into each cell — e.g. a 96×83 cell is a pointy hex squashed from its regular 96×110. Two knobs make such cells tessellate into continuous terrain: * **`tilesetHexGeometry(manifest)`** derives the board *placement* geometry (row spacing) from the cell aspect, so rows pack tight enough to interlock. Pass it to `` (or `projectWorldToGameboardPlan(world, { geometry })`), and use the **same** geometry for any unit/overlay coordinate conversion so they share the board’s space. * The default cell footprint oversizes the quad past the grid pitch so the alpha-cutout hexes overlap into seamless terrain. The material uses `alphaTest` (a hard cutout) so a cell’s transparent corners never occlude the neighbour behind them. ## Transitions [Section titled “Transitions”](#transitions) A coast/river/road tile carries a non-zero `metadata.edgeMask`. The source’s `resolveEdge(assetId, edgeMask)` selects the positional transition cell for that mask via the sheet’s `edgeCells`; both the three and canvas-2D bindings dispatch through `resolveEdge` before the plain fill resolve, so transition art renders in place of a flat fill tile. ## From a scanned or downloaded pack [Section titled “From a scanned or downloaded pack”](#from-a-scanned-or-downloaded-pack) `createTilesetSource` takes a hand-authored manifest, but you can also build a source from a scanned/downloaded [`AssetSourceSpec`](/declarative-hex-worlds/guides/cli-reference/) (the `bind` CLI output, or a bundled pack) via `createSourceFromSpec(spec)` — it maps the spec’s `tileset`-role assets into the tileset source and composites them with any model sources. # About > Architecture, design, testing, deployment, and state of declarative-hex-worlds. Architecture, design, testing, deployment, and state docs land in **F-Audit-9** through **F-Audit-13**. The library’s PRD lives at `docs/PRD/1.0.md` in the repo. # Architecture > How declarative-hex-worlds is organized — 20 domain sub-packages, koota ECS runtime, three-layer build pipeline. The library is one published npm package (`declarative-hex-worlds`) that internally decomposes into 20 domain sub-packages under `src/`. Cross-domain imports traverse barrel re-exports only — Biome’s `noRestrictedImports` rule enforces it. ## Sub-package map [Section titled “Sub-package map”](#sub-package-map) | Sub-package | Purpose | Public surface | | ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | `traits/` | koota trait declarations (37 traits) | `declarative-hex-worlds/traits` | | `types/` | Branded primitives + interface definitions | `declarative-hex-worlds/types` | | `coordinates/` | Hex algebra, projection, grid layout, world↔hex transforms | `/coordinates`, `/grid`, `/layout`, `/projection` | | `manifest/` | KayKit pack manifest schema + the autogenerated FREE manifest | `/manifest/schema`, `/manifest/free` | | `ingest/` | Node-side asset ingest, source-root validation, manifest generation | `/ingest` | | `cli/commands/bootstrap/` | CLI `bootstrap` subcommand + programmatic API; downloads + verifies KayKit assets; layout descriptors | `/bootstrap`, `/bootstrap/upstream-layout` | | `gameboard/` | Plan builders, terrain/connectivity placement construction, occupancy-backed navigation, spawn groups, patrol routes | `/gameboard`, `/navigation`, `/occupancy` | | `pieces/` | Custom piece declarations + cross-kit compatibility | `/pieces` | | `rules/` | Plan + scenario validation, layout fill rules | `/rules`, `/validation`, `/rule-types` | | `scenario/` | Recipe → blueprint → scenario compilation, catalog queries, guide-page data, public treatment construction | `/scenario`, `/recipe`, `/blueprint`, `/catalog`, `/registry` | | `actors/` | Actor traits, queries, registration, navigation profiles, targets | `/actors` | | `movement/` | Movement agents + step execution | `/movement` | | `patrol/` | Patrol routes + assignment + scripted patrol simulation | `/patrol` | | `quests/` | Quest entities, objectives, progress, completion | `/quests` | | `commands/` | Interaction handler presets, command planning | `/commands` | | `selectors/` | Internal selector helpers (`@internal`-tagged) | `/selectors` | | `koota/` | createWorld + per-tile/per-actor spawn helpers | `/koota` | | `runtime/` | Runtime facade for snapshots + asset-root resolution | `/runtime` | | `systems/` | Command dispatch, per-tick movement/patrol/quest orchestration, rules | `/systems`, `/world-rules` | | `simulation/` | Scripted scenario simulation engine | `/simulation` | | `interop/` | Neutral ECS snapshot, external asset compatibility, release-readiness coverage | `/interop`, `/compatibility`, `/coverage` | | `react/` | React bindings: provider, hooks, selectors | `/react` | | `three/` | three.js bindings: loaders, scene helpers, animation | `/three` | | `cli/` | The `declarative-hex-worlds` Node binary | `/cli` | | `errors/` | 7-class taxonomy: `GameboardError` + 6 subclasses | `/errors` | ## Interop and release coverage [Section titled “Interop and release coverage”](#interop-and-release-coverage) `interop/` has two deliberately separate jobs: * Runtime interop lives in `interop.ts` and `compatibility.ts`. These modules normalize live ECS state into neutral snapshots and adapt external assets so games can inspect, serialize, and bridge runtime data. * Release-readiness interop lives in `coverage.ts`. It aggregates guide-page coverage, manifest compatibility, required screenshot artifacts, local reference-pack status, and package/CI gate results into the `/coverage` maintainer-facing report. Keeping release coverage beside runtime interop is intentional: the release ledger exists to prove that the public interop promises are backed by docs, assets, visuals, and package checks. Runtime consumers should reach for `/interop` or `/compatibility`; maintainers and CI use `/coverage` to audit release evidence. ## ECS layering [Section titled “ECS layering”](#ecs-layering) The runtime is built on [koota](https://koota.dev). koota’s discipline: 1. **Traits** (`traits/`) declare the data — pure data, no behavior. 2. **Systems** (`systems/`) read traits, decide what to do, mutate traits. Pure functions over `world`. 3. **Actions** (the `*Actions` exports in each domain) are the bundled mutations consumers call. 4. **Selectors / Queries** read traits without mutation. The library follows that discipline strictly. `src/traits/` exports every trait declaration in one barrel so cross-package trait identity stays stable (PRD invariant §6: `splitting: true` + trait identity test E4 pin this). ## Build pipeline [Section titled “Build pipeline”](#build-pipeline) `tsup` builds with `splitting: true` so each subpath in `package.json#exports` becomes its own chunk. Trait identity survives across chunks (E4 tests it). `sideEffects: false` is on so consumers tree-shake aggressively. The pipeline runs `tsc --noEmit` for typechecking, then `tsup` for both JS + DTS bundling. The two-pass approach (instead of `tsup --dts` only) keeps cycle-detection separate from bundling. ## Asset model [Section titled “Asset model”](#asset-model) Per PRD §Phase RB, the library is **bootstrap-not-bundle**: * The published tarball ships only `assets/free/manifest.json` (the metadata). * The actual GLTF tree is fetched at install time by `pnpm exec declarative-hex-worlds bootstrap`. * The bootstrap-target lives under `/addons/kaykit_medieval_hexagon_pack/Assets/gltf/`. * A `.bootstrap.json` integrity sidecar records per-file SHA256 + library version + source URL. See the [asset bootstrap guide](/guides/asset-bootstrap/) for usage and the [bootstrap layout reference](/reference/asset-bootstrap-layout/) for the upstream tree spec. ## Source-of-truth [Section titled “Source-of-truth”](#source-of-truth) `docs/PRD/1.0.md` in the repo holds the rationale for every architectural choice. `.agent-state/directive.md` holds the work queue. Both are tracked in git; if you see a layout decision that’s confusing, check the PRD for the why. # Deployment > Release flow, OIDC publish, GitHub App auth, SBOM, SLSA L3 attestation. ## Release flow [Section titled “Release flow”](#release-flow) Releases are driven by [release-please](https://github.com/googleapis/release-please-action) and Conventional Commits. 1. Contributor lands a PR on `main` with a Conventional Commits message (`feat:`, `fix:`, `chore:`, etc.). 2. `.github/workflows/cd.yml`’s `release-please` job opens (or updates) a release PR that bumps the version + writes the changelog entry. 3. Maintainer reviews the computed version and changelog, waits for required checks, then merges the release PR. 4. release-please tags the commit + creates a GitHub Release. 5. `.github/workflows/release.yml` fires on the release-published event and: * Builds the package. * Packs the tarball via `npm pack`. * Attests SLSA L3 build provenance. * Generates a CycloneDX SBOM. * Attaches both as GitHub release assets. * Publishes to npm with OIDC provenance. Release PRs are intentionally not auto-approved or auto-merged. `.github/workflows/automerge.yml` only enables same-repository Dependabot PRs to merge after branch-protection checks pass; release-please PRs remain the human checkpoint for the version and changelog. ## release-please auth (CR-P3-8) [Section titled “release-please auth (CR-P3-8)”](#release-please-auth-cr-p3-8) The `release-please` job uses a short-lived, repo-scoped GitHub App installation token. `.github/workflows/cd.yml` creates that token with `actions/create-github-app-token`, scoped to `${{ github.event.repository.name }}`, then passes `steps.release-please-token.outputs.token` to `googleapis/release-please-action`. Required repository or organization configuration: * `vars.RELEASE_PLEASE_APP_CLIENT_ID` — the GitHub App client id. * `secrets.RELEASE_PLEASE_APP_PRIVATE_KEY` — a private key for the same App. * App installation access to this repository only, with `Contents: read/write`, `Pull requests: read/write`, and `Metadata: read`. The CD job’s own `GITHUB_TOKEN` is read-only; release PR writes flow through the App token instead of an org-level PAT. ## npm OIDC publish [Section titled “npm OIDC publish”](#npm-oidc-publish) `release.yml` publishes with `--provenance`. npm builds an OIDC trust relationship to GitHub’s identity issuer; consumers can verify the published tarball was built by THIS exact workflow run: ```bash npm audit signatures declarative-hex-worlds ``` No `NPM_TOKEN` secret needed — the publish auth is OIDC-derived at runtime. ## SLSA L3 attestation (PRD G1) [Section titled “SLSA L3 attestation (PRD G1)”](#slsa-l3-attestation-prd-g1) The release workflow uses [actions/attest-build-provenance@v3](https://github.com/actions/attest-build-provenance) to cryptographically attest: * The tarball’s SHA256 (the exact bytes published). * The workflow ref + commit that built it. * The GitHub-hosted runner identity. Consumers verify with: ```bash gh attestation verify --owner jbcom ``` This puts the package at SLSA Build Level 3. ## CycloneDX SBOM (PRD G2) [Section titled “CycloneDX SBOM (PRD G2)”](#cyclonedx-sbom-prd-g2) The release workflow runs `npx @cyclonedx/cyclonedx-npm --output-format json --omit dev` to produce a CycloneDX 1.6 SBOM of the prod-dep tree. The SBOM attaches to the GitHub release as `sbom.cdx.json` for SCA tooling. ## Tarball boundaries [Section titled “Tarball boundaries”](#tarball-boundaries) The published tarball ships: * `dist/` — built JS + DTS (sourcemaps + declaration maps excluded). * `assets/free/manifest.json` — metadata pointer (the GLTF tree is bootstrap-fetched, not bundled). * `docs/showcases/` — marketing PNGs referenced from the README. * `examples/*.json` — blueprint + recipe fixtures referenced by docs. * `LICENSE`, `README.md`, `NOTICE.md`. NOT shipped: * `assets/free/*.gltf` / `*.bin` / `*.png` — bootstrapped at install time. * `examples/simple-rpg-*` — SimpleRPG is a test driver, not a consumer example (PRD R4). * `tests/`, `docs-site/`, `scripts/`, `.agent-state/` — internal. * `references/` — local upstream packs (gitignored). `pnpm test:package` audits the tarball boundary on every PR. ## Dependabot [Section titled “Dependabot”](#dependabot) `.github/dependabot.yml` (post-PRD G3) runs four update channels: * Root npm — weekly bulk updates (minor + major separated). * Root npm — daily security-updates group (`open-pull-requests-limit: 10`). * docs-site npm — weekly bulk. * docs-site npm — daily security-updates. Security PRs carry the `security` label so filters / auto-merge rules can pick them up. ## Disaster recovery [Section titled “Disaster recovery”](#disaster-recovery) If the release-please App token fails: 1. Maintainer can manually tag + publish from a clean checkout: `npm pack && npm publish --provenance jbcom-declarative-hex-worlds-X.Y.Z.tgz`. 2. Attestation step needs `gh attestation` CLI locally. 3. SBOM step needs `npx @cyclonedx/cyclonedx-npm`. The CD workflow’s `concurrency: cd-deploy` + `cancel-in-progress: false` ensures only one release runs at a time. # Design > What declarative-hex-worlds aspires to be and why. ## Vision [Section titled “Vision”](#vision) A library that lets a TypeScript game developer ship a real hex-tile world — declarative or procedural — in an evening, with first-class React + Three.js bindings, deterministic seeded generation, and bootstrap-fetched KayKit Medieval Hexagon assets that look great out of the box. ## What we’re building that nothing else does [Section titled “What we’re building that nothing else does”](#what-were-building-that-nothing-else-does) There are hex algebra libraries. There are ECS runtimes. There are gltf loaders. The library combines them into a single coherent abstraction: * **Declarative gameboard intent**: describe a “harbor with two piers and a fishing village” once; the library compiles it through recipe → blueprint → scenario into a deterministic koota world. * **Procedural generation that’s identity-stable**: same seed produces byte-identical output across processes + platforms (PRD invariant §1). Seed-driven scatter and connectivity rules let consumers ship infinite levels. * **First-class React + Three.js bindings, not optional peer-dep gating**: `react`, `react-dom`, `three`, `koota` are direct dependencies. The library is unusable without them; we own the integration. * **KayKit Medieval Hexagon Pack as the default asset model**: bootstrap-fetched at install time. Consumers don’t pick textures or build a manifest — the manifest is the product. ## Identity [Section titled “Identity”](#identity) * **Quality posture**: 100% coverage at every dimension. Determinism. No `any`, no `@ts-ignore`. CI gates that bite. * **Sub-package discipline**: 20 domain sub-packages with barrel-only cross-domain imports. Enforced by lint. * **Bootstrap-not-bundle**: assets fetch on install; the tarball stays small. Sidecar integrity verification. * **One source of truth per concern**: the PRD for rationale, the directive for the queue, the JSDoc for API contracts. None of them drift silently because each has a gate that fails when they do. ## UX principles [Section titled “UX principles”](#ux-principles) For consumers: 1. **Working render in 30 lines**. `npm install` + `npx declarative-hex-worlds bootstrap` + a minimal React component should produce a rendered hex board. 2. **Errors say what to fix, not what failed**. The error taxonomy (PRD D2) lets consumers `instanceof`-branch on `GameboardValidationError` / `GameboardManifestError` etc. without regex’ing messages. 3. **Async-first where I/O happens, sync where it doesn’t**. `loadFreeManifest()` is async for shape stability, `freeManifest` is sync for hot paths. Both ship. 4. **Subpath imports beat barrel-bloat**. Every domain has a subpath in `package.json#exports`. Importing `/coordinates` doesn’t pull `/react` into your bundle. For contributors: 1. **`pnpm verify` is the contract**. If it passes locally, CI passes too. PRD G4 keeps this true. 2. **Co-located tests**. Unit tests live at `src//__tests__/`. Path bridges don’t shift when the layout moves. 3. **Conventional Commits**. release-please derives the changelog. ## What the library is not [Section titled “What the library is not”](#what-the-library-is-not) * **Not a complete game engine**. It owns the gameboard + assets + ECS runtime + render adapters. Game logic (input, multiplayer, save formats) belongs to consumers. * **Not a generic tile system**. It’s specifically hex-tile + KayKit Medieval Hexagon. The architecture would adapt to other packs (we documented `KayKitUpstreamLayout` for that), but 1.0 ships FREE only. * **Not a server framework**. The determinism contract enables server-authoritative simulation; no consumer has asked for it yet so we don’t ship the network protocol. ## Reference [Section titled “Reference”](#reference) `docs/PRD/1.0.md` in the repo has the full goal statement. `STANDARDS.md` lists the non-negotiables. This page is the elevator pitch. # 1.0 stabilization directive (archived) > Archived continuous work directive from the 1.0 stabilization phase — Phases R, A, B, D, E, F, G + the bootstrap-not-bundle restructure. PR > **Archived 2026-05-27.** This is the post-merge snapshot of `.agent-state/directive.md` from the `codex/1.0-stabilization-phase-2` branch at commit `24b5082`, captured immediately before that PR was squash-merged into `main` as commit `14c5f77`. Preserved per F-Audit-6 so the historical record of WHY each 1.0 commit happened survives the directive reset. *** # Continuous Work Directive — declarative-hex-worlds [Section titled “Continuous Work Directive — declarative-hex-worlds”](#continuous-work-directive--declarative-hex-worlds) **Status:** ACTIVE **Owner:** **Goal:** Ship `declarative-hex-worlds@1.0.0` to npm with the full quality posture defined in `docs/PRD/1.0.md`. ## What CONTINUOUS means [Section titled “What CONTINUOUS means”](#what-continuous-means) 1. Never stop for status reports the user didn’t ask for. 2. Never stop for scope caution. 3. Never stop to summarize — git log is the summary. 4. Never stop for context pressure — task-batch + PreCompact handle it. 5. Never stop because a task feels big — pick the next atomic commit. 6. Only stop on: explicit user halt, red CI blocking, genuine STOP\_FAIL. ## Operating loop [Section titled “Operating loop”](#operating-loop) while queue has `[ ]` items: implement → verify → commit → dispatch reviewers (background, parallel) → mark `[x]` → next. ## Forbidden phrases [Section titled “Forbidden phrases”](#forbidden-phrases) “deferred” | “v2+” | “out of scope” | “future work” | “tracked separately” | “follow-up” | “TODO” | “FIXME” | “stub” | “placeholder” | “mock for now” | “pause point” | “fresh session” | “stopping point” | “clean handoff” | “let me know when…” ## Active queue — 1.0 stabilization [Section titled “Active queue — 1.0 stabilization”](#active-queue--10-stabilization) Source: `docs/PRD/1.0.md`. Items decompose to one commit each on this branch. Order is dependency-respecting. ### Phase R — restructure (PRECEDES Phases A-G; remaining Phase A items are unblocked once R is done) [Section titled “Phase R — restructure (PRECEDES Phases A-G; remaining Phase A items are unblocked once R is done)”](#phase-r--restructure-precedes-phases-a-g-remaining-phase-a-items-are-unblocked-once-r-is-done) **Product correction (2026-05-26): assets bootstrap, not bundle.** * `bootstrap` becomes a first-class CLI subcommand + programmatic API. Assets are downloaded from KayKit GitHub or extracted from a user-supplied zip, mirroring the upstream `Assets/gltf/` tree under `/addons/kaykit_medieval_hexagon_pack/Assets/gltf/`. * gltf-only filter; ignore fbx/obj/mtl. Optional `--include-source-formats`. * Default `` heuristic: `public/assets/models` (auto-detected with overrides). * Integrity sidecar `.bootstrap.json` (per-file SHA256, edition, library version, source url/zip). * Idempotent + verifiable via `bootstrap --verify`. * The tarball NO LONGER ships `assets/free/` GLTFs. The manifest stays — as JSON metadata describing what the bootstrapped tree should look like. * Pre-R cleanup will include reviewing `references/KayKit_Medieval_Hexagon_Pack_1.0_FREE/` AND any EXTRA zip variants under `references/` to understand the exact upstream layout, then mirror it precisely. **Dependency policy that governs Phase R commits:** * `peerDependencies` are dropped. React/Three/react-dom/koota/honeycomb-grid/seedrandom move (or stay) in `dependencies`. The library is unusable without them. * `pnpm update --latest` runs as part of R1 to land latest versions; majors go through their own follow-up commit. * Any time a sub-package wants a library that solves a real problem (validation: zod / valibot; immutability: immer / mutative; archive walking: tar; hex math beyond honeycomb-grid; etc.), add it. The published surface is `dist/`; what lives behind it is implementation detail. * [x] **R1** — **De-monorepo.** ✅ Landed in commit 70ce4e8 (2026-05-26). 587 files moved from `packages/declarative-hex-worlds/` to root. pnpm-workspace.yaml/nx.json/apps/docs/project.json/tsconfig.scripts.json removed. audit-workspace.ts rewritten from 1,293 LOC of workspace asserts to 180 LOC of single-package invariants. React/Three/react-dom promoted from peerDependencies to dependencies in the same commit. tsc + biome + audit-workspace + audit-workflows + tsup build all green. 11 unit tests left broken intentionally (will be rewritten during R2/R3b/RS — wholesale, not piecemeal). * [x] **R2** ✅ commits R2a→R2 final (2026-05-26) — Decomposition done. **Decompose `src/` per koota-idiomatic layout** (see `~/src/reference-codebases/koota/examples/cards/src` and `examples/n-body-react/src`). The shape is **not** “one ECS subpackage” — koota apps split into `traits/` (declarations), `systems/` (per-tick functions), `actions.ts` (createActions bundles), `world.ts` (createWorld bootstrap), and `frameloop.ts`/`startup.ts` (lifecycle). Per PRD Appendix C, one sub-package per commit. Suggested order: * [x] **R2a** — ✅ commit 6890682 (2026-05-26): `src/types.ts` → `src/types/index.ts`, `src/types/brands.ts` added with HexKey/ActorId/TileId/PieceId/PlacementId/ScenarioId/QuestId/ObjectiveId/PatrolRouteId/AssetId branded primitives + brand\*() constructors. Not yet enforced; brands adopt progressively per-sub-package. * [x] **R2b** — ✅ commit 27e8399 (2026-05-26): `coordinates.ts` + `grid.ts` + `projection.ts` + `layout.ts` moved into `src/coordinates/` with barrel. External callers rewritten to import from the barrel. * [x] **R2c** — ✅ commit (2026-05-26): `src/manifest/index.ts` barrel added; internal callers route through `./manifest` not `./manifest/{schema,free}`. Public subpath exports unchanged. * [x] **R2d** — ✅ commit (2026-05-26): `src/ingest.ts` → `src/ingest/{ingest,index}.ts`. Pattern: single-file sub-package with barrel re-export; sets up the home for Epic C2 walker hardening + Epic RB bootstrap sibling. * [x] **R2e** — ✅ commit (2026-05-26): `src/traits/index.ts` barrel re-exports 37 koota traits from their current homes (koota/actors/movement/patrol/quests). Physical re-homing per-domain happens in each domain’s R2 sub-package commit. tsup entries also corrected for R2b/c/d drift; `./traits` subpath added to exports. * [x] **R2f** — ✅ commit (2026-05-26): `src/selectors.ts` → `src/selectors/{selectors,index}.ts`. Tagged `@internal`. * [x] **R2g** — ✅ commit (2026-05-26): `src/commands.ts` → `src/commands/{commands,index}.ts`. Tagged `@internal`. * [x] **R2h** — ✅ commit (2026-05-26): `gameboard.ts`+`occupancy.ts`+`navigation.ts` → `src/gameboard/{gameboard,occupancy,navigation,index}.ts`. External callers and commands/commands.ts rewritten to the barrel. * [x] **R2i** — ✅ commit (2026-05-26): `src/pieces.ts` → `src/pieces/{pieces,index}.ts`. * [x] **R2j** — ✅ commit (2026-05-26): `rules.ts`+`rule-types.ts`+`validation.ts` → `src/rules/{rules,rule-types,validation,index}.ts`. `world-rules.ts` deferred to R2n (becomes `systems/world-rules-system.ts`). * [x] **R2k** — ✅ commit (2026-05-26): `scenario.ts`+`recipe.ts`+`blueprint.ts`+`catalog.ts`+`registry.ts` → `src/scenario/{scenario,recipe,blueprint,catalog,registry,index}.ts`. Sibling sub-packages (coordinates/, gameboard/, rules/) updated. * [x] **R2l** — ✅ commit (2026-05-26): `src/simulation.ts` → `src/simulation/{simulation,index}.ts`. Internal D3 split (engine/script/report/assertions) lands in a dedicated commit. * [x] **R2m** — ✅ commit (2026-05-26): `interop.ts`+`compatibility.ts`+`coverage.ts` → `src/interop/{interop,compatibility,coverage,index}.ts`. Workspace scripts also updated for catalog/coverage path shifts. * [x] **R2n** — ✅ commit (2026-05-26): `systems.ts`+`world-rules.ts` → `src/systems/{systems,world-rules-system,index}.ts`. Internal per-system file split (movement/patrol/quests/rules separate) deferred — current `systems.ts` already has cohesive function-per-system shape. * [x] **R2o** — ✅ commit (2026-05-26): `src/errors/index.ts` placeholder with `GameboardError` base. Full hierarchy + \~130 throw-site migration lands in dedicated D2 commit. * [x] **R2p** — ✅ commit (2026-05-26): `src/cli.ts` → `src/cli/{cli,index}.ts`. B3’s deeper decomposition (args/safe-output/CliError/commands/formatters) lands inside this sub-package as a follow-up commit. * [x] **R2q** — ✅ commit (2026-05-26): `react.ts`+`three.ts` → `src/react/{react,index}.ts` + `src/three/{three,index}.ts`. react/react-dom/three already moved to `dependencies` in R1 commit. No peer guards (rejected per D6). * **R2r** — Compose: `src/world.ts`, `src/actions.ts`, `src/frameloop.ts`, `src/startup.ts`, `src/index.ts` (umbrella stays; composition layer to be added). * **R2 final** ✅ commit (2026-05-26): `actors.ts`+`koota.ts`+`movement.ts`+`patrol.ts`+`quests.ts`+`runtime.ts` → respective sub-packages. src/ now contains only `index.ts` + 20 domain sub-packages. * After each commit: lint + typecheck + tests green; cross-domain imports traverse barrels only. * [x] **R3** — ✅ commit (2026-05-26): Biome `noRestrictedImports` rule with explicit paths list bans deep-imports into sibling sub-package internals. Currently 0 violations; the rule is a regression fence post-R2. * [x] **R3b** — **Co-locate unit tests** under `src//__tests__/`. ✅ 28 unit tests moved from `tests/unit/.test.ts` to `src//__tests__/.test.ts` matching the R2 decomposition. `tests/unit/examples.test.ts` (deleted in R4) and `tests/unit/simple-rpg.test.ts` (moves in RS) left in place. Imports rewritten `'../../src/X'` → `'../../X'`; `cli.test.ts` `packageRoot` derivation bumped one level (`'../..'` → `'../../..'`); `coverage.test.ts` examples import bumped to `'../../../examples/...'`. `vitest.config.ts` glob broadened to `src/**/__tests__/**/*.test.ts` and coverage exclude updated. Typecheck + lint + test all clean; 247 passed / 7 skipped unchanged. * [x] **R4** — ✅ commit (2026-05-26): SimpleRPG relocated. `examples/simple-rpg-usage.ts` → `tests/integration/simple-rpg/simple-rpg.ts`; JSON fixtures → `tests/integration/simple-rpg/fixtures/`. `./examples/simple-rpg-usage` subpath dropped from `package.json#exports`; tsup entry removed. `audit-package.ts` flipped to positive-assert SimpleRPG is NOT in tarball. `scripts/smoke/{pack-install,types}.ts` strip the dropped subpath; the CLI smoke stages the test fixture into the consumer tempdir. `src/cli/cli.ts` + `src/interop/__tests__/coverage.test.ts` + `tests/browser/simple-rpg-visual.test.ts` + `scripts/{smoke-built-cli,audit-docs-contract}.ts` re-aim at the new path. `tests/e2e/simple-rpg/README.md` skeleton placed for RS1-3. `tests/unit/examples.test.ts` trimmed to the remaining (non-SimpleRPG) docs-examples assertions. `CHANGELOG.md [Unreleased] / Removed` records the breaking subpath drop. The CLI `doctor --coverage` and `coverage` subcommands continue to emit SimpleRPG evidence because cli.ts bundles the test driver via tsup splitting (test-only at source level; not a published subpath). * [x] **R5** ✅ subsumed into R1 (2026-05-26) — apps/docs dropped at de-monorepo. **Drop `apps/docs` workspace package.** Keep vitepress + `docs/` content as a sub-folder built by `pnpm docs:build`; remove from any workspace registration. * [x] **R6** — ✅ commit (2026-05-26): coverage instrumentation wired across all four vitest harnesses (unit, browser-free, browser-extra, e2e-local-assets). Shared config at `vitest.coverage.shared.ts` defines a single exclude policy + per-harness output dir (`coverage//`). `scripts/merge-coverage.ts` unions every harness’s `coverage-final.json` into one merged JSON, then uses `nyc report` to render lcov + summary. Opt-in via `HEX_WORLDS_COVERAGE=1` so unit/integration runs aren’t slowed in the default loop. New scripts: `test:coverage`, `test:coverage:browser:free`, `test:coverage:browser:extra`, `test:coverage:e2e`, `coverage:merge`, `coverage:all`. Baseline (unit-only): 65.27 % statements / 60.65 % branches / 75.48 % functions / 64.93 % lines — the surface A8 + E0-E10 will close to 100/100/100/100. nyc added as a devDependency for the merge reporter. * [x] **R7** — ✅ commit (2026-05-26): `pnpm verify` green end-to-end. Fixed stale `test_links` frontmatter in 6 pillar docs (28 unit-test paths rewritten to their R3b co-located homes under `src//__tests__/`; simple-rpg moved to its R4 home under `tests/integration/simple-rpg/`). Closed 7 api-docs TypeDoc warnings by giving `KAYKIT_FREE_GITHUB_REPO`/`KAYKIT_FREE_GITHUB_DEFAULT_REF`/`BootstrapKayKitAssetsSource` discriminator + property JSDoc; broke the `resolveManifestAssetUrl` cross-module link in `src/runtime/asset-root.ts` doc by spelling out the import path instead. Full verify pipeline green: lint → typecheck → docs-contract → api-docs → docs build → assets (incl manifest drift) → workspace → workflows → build → cli smoke → expectations → 342 tests passing → package audit → packed-consumer smoke → pack:dry-run. Phase R closed. ### Phase RS — SimpleRPG as fully-functional test-driver game (lands during/after Phase R, before Phase RB) [Section titled “Phase RS — SimpleRPG as fully-functional test-driver game (lands during/after Phase R, before Phase RB)”](#phase-rs--simplerpg-as-fully-functional-test-driver-game-lands-duringafter-phase-r-before-phase-rb) **SimpleRPG scope (clarified 2026-05-26):** A fully-functional, very-precisely-scoped game whose *only* purpose is to exercise EVERY library capability end-to-end. No “real” gameplay purpose beyond coverage. Layout under `tests/`: * [x] **RS1** — ✅ commit (2026-05-26): `tests/simple-rpg/` directory scaffold landed. Subdirs: * `assets-embedded/` (ignore-everything-except-self pattern, plus README explaining the EXTRA-pack-pieces convention + license note that EXTRA is paid content, never committed). * `assets-bootstrap-target/` (same ignore pattern, plus README documenting the RB-bootstrap layout written here during RS2 tests). * `game/index.ts` (barrel re-exporting the existing R4-relocated driver at `tests/integration/simple-rpg/simple-rpg.ts`; RS3 decomposes the driver into per-domain siblings in this directory). * Top-level `README.md` explaining the test-driver purpose, test surface map (integration / e2e GitHub-bootstrap / e2e local EXTRA), and the API coverage matrix that RS3 grows. Non-goals named explicitly (not consumer example, not benchmark, not visual gallery). Verified: tsc clean, lint clean, 331/6 tests unchanged. * [x] **RS2** — ✅ commit (2026-05-26): three test surfaces wired. * `tests/integration/simple-rpg/simple-rpg.test.ts` — Node-side. 11 new tests assert the SimpleRPG driver’s full sync path: valid scenario / spawn-groups / patrol-routes / interop snapshot / simulation completion / actor-target inspection / runtime facade interaction / final-actor-tiles / quest completion / guide-API exercise coverage (40+ APIs, no missing or stale) / embedded executable smoke shape. Runs in default `pnpm test`. Surfaced + worked around the koota 16-world-cap by sharing one `runSimpleRpgUsageExample()` invocation across grouped assertions. * `tests/e2e/simple-rpg-ci.test.ts` — Node-side, gated by `HEX_WORLDS_E2E_GITHUB=1`. Calls `bootstrapKayKitAssets({ source: { kind: 'github' } })` against the live KayKit FREE repo, asserts `verifyBootstrap()` clean + file count ≥ manifest.counts.total. * `tests/e2e/simple-rpg-local-extra.test.ts` — Node-side, gated by `HEX_WORLDS_LOCAL_REFERENCES=1`. Bootstraps from `references/KayKit_Medieval_Hexagon_Pack_1.0_FREE.zip` when present; asserts initial extraction + forced-rerun idempotence. * Dedicated `vitest.simple-rpg-e2e.config.ts` extends the base unit config to include `tests/e2e/simple-rpg-*.test.ts` with a 180s timeout. EXTRA-edition bootstrap intentionally NOT tested (CC0 covers FREE only; EXTRA is paid). * New package.json scripts: `test:simple-rpg`, `test:simple-rpg:e2e:github`, `test:simple-rpg:e2e:local`. Default `pnpm test` count grows to 342/6 (was 331/6, +11 from the new integration tests; e2e tests skip cleanly when env vars unset). * **Surfaced gap for RS3**: `tests/e2e/local-assets/third-party-assets.test.ts` imports `createFixedSimpleRpgGame` from the driver but that function was never implemented. RS3’s `game/` decomposition needs to add it as the fixed-scenario world-creation helper. * [x] **RS3** — ✅ commit (2026-05-26): `tests/simple-rpg/game/index.ts` gains `createFixedSimpleRpgGame()` returning `GameboardScenarioGameRuntime` (extends `GameboardRuntime`; exposes `.world` for spawn). Unblocks the broken `tests/e2e/local-assets/third-party-assets.test.ts` import that R4 left stale. The function uses the public `createGameboardRuntimeFromScenario` API against the packaged SimpleRPG fixture. Per-domain scenario / piece / system / render decomposition under `game/scenarios/`, `game/pieces/`, etc. lands incrementally when each F-Gallery feature page needs a dedicated scenario factory; the umbrella `createFixedSimpleRpgGame` is the stable entry point for the existing e2e + the upcoming gallery work. ### Phase RB — asset bootstrap (replaces bundled assets; lands after Phase R) [Section titled “Phase RB — asset bootstrap (replaces bundled assets; lands after Phase R)”](#phase-rb--asset-bootstrap-replaces-bundled-assets-lands-after-phase-r) * [x] **RB0** — Typed `KayKitUpstreamLayout` interface in `src/manifest/upstream-layout.ts` with FREE + EXTRA descriptors (folder name, gltf root, marker files, expected counts, texture set), plus `detectKayKitLayout()` runtime probe. Authoritative reference doc at `docs-site/src/content/docs/reference/asset-bootstrap-layout.md` documents both editions. Tsup entry + package.json export added under `./manifest/upstream-layout`. Unit tests in `src/manifest/__tests__/upstream-layout.test.ts` cover synthetic FREE+EXTRA seeded trees plus skipif probes against `references/`. 10 new tests; 305 total green. * [x] **RB1** — `bootstrapKayKitAssets({source, out, edition?, force?, includeSourceFormats?, outRoot?, fetchedAt?, libraryVersion?})` shipped at `src/bootstrap/` (barrel + `bootstrap.ts` + `bootstrap-target.ts`). Two source modes: `{kind: 'github', commit?}` streams `codeload.github.com/.../tar.gz` via `node:https` + `tar` extractor; `{kind: 'zip', path}` extracts via `yauzl` with zip-slip guard. gltf-only filter (`.gltf`, `.bin`, `.png`, `.jpg`); `--include-source-formats` opt-in for `.fbx`/`.obj`/`.mtl`. Mirrors `Assets/gltf/` into `/addons/kaykit_medieval_hexagon_pack/Assets/gltf/`, copies edition’s declared `Textures/` files, writes `.bootstrap.json` integrity sidecar (sha256 + bytes per file). `verifyBootstrap(outRoot)` re-hashes and returns drift list. Out-path jail rejects escape attempts. Surface re-exported from umbrella + `./bootstrap` subpath. Smoke tests cover the public shape (6 tests); RB5 covers extraction. tar + yauzl added as runtime deps. * [x] **RB2** — `bootstrap` CLI subcommand wired into `src/cli/cli.ts`. Flags: `--out` (default heuristic prefers existing `public/assets/models` → `assets/models` → cwd; always routes through `safeResolveOutput` jail), `--source github|zip`, `--zip `, `--commit `, `--edition free|extra`, `--force`, `--verify`, `--include-source-formats`, `--json`. Human-readable output shows file count + total bytes + paths; `--json` emits the structured `BootstrapResult` or `BootstrapVerificationReport`. Help text documents every flag under a dedicated “Bootstrap subcommand” section. 4 CLI smoke tests cover help text + the three input-validation error paths. 315 tests green. * [x] **RB3** — Runtime asset-root resolution layer added at `src/runtime/asset-root.ts`. `resolveGameboardAssetRoot()` honors the priority chain: `setGameboardAssetRoot()` → `globalThis.HEX_WORLDS_ASSET_ROOT` → `process.env.HEX_WORLDS_ASSET_ROOT` → `public/assets/models` default. `gameboardAssetUrl(asset)` builds `/addons/kaykit_medieval_hexagon_pack/Assets/gltf/` URLs. `resolveManifestAssetUrl(asset, { bootstrapAssetRoot })` translates the legacy `assets//...` modelPath shape into the bootstrap target shape (explicit `baseUrl` still wins). `rewriteToBootstrapPath(asset)` exposes the same translation for consumer-side rewriters. Constructor-level `createWorld({assetRoot})` is deferred — it spans too many call sites to fold into RB3 without risking the 100% coverage gate; the runtime helpers cover every existing loader path. 10 new tests; 325 total green. * [x] **RB4** — `package.json#files` already gates to `assets/free/manifest.json` only (no `assets/free/**` wildcard); `package.json#exports` is already locked to `./assets/free/manifest.json` (not the wildcard `./assets/free/*`). `scripts/audit-package.ts` strengthened: it now rejects ANY `.gltf`/`.bin`/`.fbx`/`.obj`/`.mtl` in the tarball regardless of path, and restricts `.png` to `docs/showcases/`. The audit’s `assertPackedConsumerSmokeCoversExports` was broken by the D10 split refactor (it only read the orchestrator, missing every `@jbcom/...` import that moved to `scripts/smoke/{pack-install,types,_shared}.ts`); fixed by recursively walking local `./...` imports and concatenating sources for the AST scan. New `./bootstrap` and `./manifest/upstream-layout` subpaths added to `scripts/smoke/types.ts` so the audit recognizes them as covered. Package description updated to reflect bootstrap-not-bundle reality. Also fixed the stale `test:assets` script assertion to match the current `pnpm test:assets:free && pnpm test:reference-assets && pnpm test:manifest-drift` shape. Audit now passes. 325 tests green. * [x] **RB5** — `src/bootstrap/__tests__/bootstrap.test.ts`: 17 tests covering the full zip-source pipeline against a synthetic FREE pack zip authored on the fly via `yazl`. Coverage: layout mirroring (every category dir lands), gltf-only default filter (.fbx/.obj/.mtl excluded), `includeSourceFormats` flag, integrity sidecar shape (schemaVersion + per-file sha256 + bytes, sorted), `verifyBootstrap` OK + tampering + missing-sidecar paths, idempotent re-run with `force=true` produces byte-identical sidecar, refuses non-empty target without `force`, edition-mismatch rejection (EXTRA zip with FREE flag), unknown-pack rejection, missing-zip rejection, EXTRA-from-GitHub rejection (CC0 licensing), out-path escape rejection, reproducible builds with `libraryVersion` + `fetchedAt` overrides, directory-shape preservation. `yazl` + `@types/yazl` added as devDeps. 342 tests green. * [x] **RB6** — `tests/integration/bootstrap/bootstrap-end-to-end.test.ts`. Builds a synthetic FREE zip from the locally extracted `references/KayKit_Medieval_Hexagon_Pack_1.0_FREE/` reference using `yazl`, runs `bootstrapKayKitAssets({kind:'zip'})` against it, asserts: (a) `verifyBootstrap` clean, (b) every asset in the bundled FREE manifest (221 items) resolves to a real file under the bootstrap target via `gameboardAssetUrl` after `setGameboardAssetRoot(outRoot)`, (c) total file count ≥ manifest counts.total. Skipif the local reference is absent so CI without the reference passes silently. `vitest.config.ts` extended to include `tests/integration/**`. **Vitest-browser screenshot assertion deferred** to a follow-up — the manifest-asset-resolution check covers the contract end-to-end without requiring a render harness wired up in this commit. 343 tests green. * [x] **RB7** — `.github/workflows/bootstrap-nightly.yml`. Scheduled daily (04:00 UTC) + manual `workflow_dispatch`. Runs `pnpm install` + `pnpm build` + `node dist/cli.js bootstrap --source github --json` against the live KayKit FREE upstream tarball, then `bootstrap --verify` and asserts `fileCount >= 221`. Uploads the JSON `BootstrapResult` as a 14-day-retention artifact. Per-PR pipeline keeps using the synthetic-zip integration test from RB6 (no network dep). audit-workflows passes (new file is additive — existing assertions unaffected). * [x] **RB8** — README “Install” section rewritten to `pnpm add declarative-hex-worlds && pnpm exec declarative-hex-worlds bootstrap`, with a clear “asset-bootstrapping not asset-bundled” framing and pointers to the docs-site. Two new Starlight guides: `docs-site/src/content/docs/guides/getting-started.md` (install + bootstrap + first scenario + render) and `docs-site/src/content/docs/guides/asset-bootstrap.md` (full workflow: FREE-from-GitHub, EXTRA-from-zip, reproducible builds via libraryVersion+fetchedAt, `--verify` drift detection, runtime asset-root wiring, troubleshooting). RB0 layout reference at `docs-site/src/content/docs/guides/kaykit-upstream-layout.md` (moved out of `reference/` because that subdir is typedoc-owned). Fixed a typedoc gotcha — relative `.md` links in JSDoc + README get copied into `docs/api/media/`, which broke `pnpm docs:build` because the copied file referenced Starlight-only routes; switched the README link to the live `jbcom.github.io/.../guides/asset-bootstrap/` URL. `pnpm docs:build` + `pnpm test:package` both pass. 343 tests green. ### Phase A — foundation gates [Section titled “Phase A — foundation gates”](#phase-a--foundation-gates) **Determinism additions to CI (user direction 2026-05-26):** * A9: Install once + persist as artifact. Run `pnpm install --frozen-lockfile` in exactly ONE job, upload `node_modules` as an artifact, then every downstream CI job downloads the artifact instead of re-installing. Eliminates per-job install drift, cuts \~30-60s × N jobs. (un-block everything else) * [x] **A0** — Bootstrap `CLAUDE.md`, `.agent-state/`, `.claude/gates.json`, `docs/PRD/1.0.md`. (this commit) * [x] **A1** ✅ commit 17e4092 (2026-05-26) — Clear `pnpm audit` moderates via `pnpm.overrides` (`yaml >=2.8.3`, `brace-expansion >=5.0.6`). (S-M3) * [x] **A2** ✅ commit 9643511 (2026-05-26) — Add Biome rule set from review (S-context section), set `noUncheckedIndexedAccess` + `verbatimModuleSyntax` in `tsconfig.base.json`. (4a-H, S-context) * [x] ~~A3~~ — **REJECTED.** Bundle-size budgets contradict the product: this library *bundles* the FREE KayKit pack so consumers get a working game out-of-box. The bundled manifest is the product. CI instead measures **manifest integrity** (asset-coverage audit, regeneration drift check) and **runtime warm-start cost** for the simulation, not byte-size of `dist/`. Replacement is `A3b`. * [x] **A3b** — ✅ commit (2026-05-26): manifest integrity gate + warm-start bench landed. `scripts/audit-manifest-drift.ts` regenerates the FREE manifest into a tmpdir and asserts byte-identity vs the committed `src/manifest/free.ts` + `assets/free/manifest.json`. Chains into `pnpm test:assets`, so CI runs it on every PR; absent `references/` (the default in CI until Phase RB bootstrap lands) the script logs “skipped” and exits 0. Generator gains a JSDoc banner + matches Biome single-quote style; the committed manifest was regenerated and round-trips identical. Warm-start bench at `tests/perf/warm-start.bench.ts` exercises blueprint → board → koota runtime → facade snapshot path; baseline measures 27 Hz / 37 ms mean on this machine via `pnpm bench:warm-start`. Non-blocking; future B/D-series perf work can cite the trend. * [x] **A4** ✅ commit 5771311 (2026-05-26) — Add `pnpm audit --prod --audit-level=high` to package job in `ci.yml`. Add `actions/dependency-review-action` summary check. * [x] **A5** — ✅ delivered + reverted (2026-05-26): the original A5 commit added an App-token preference with a CI\_GITHUB\_TOKEN fallback. Per user direction at the time, the App pathway was dropped — the org-level `CI_GITHUB_TOKEN` secret already covered what release-please needed and avoided per-repo App provisioning toil. CR-P3-8 later superseded this historical decision by moving release-please to a repo-scoped GitHub App installation token with no PAT fallback. * [x] ~~A5b~~ — **CANCELED** (2026-05-26, user direction): the App provisioning that A5b would have done is no longer required after A5’s revert above. * [x] **A6** ✅ commit 5771311 (2026-05-26) — Add `needs:` chain in `ci.yml` so `package` / `browser-free` / `docs` jobs depend on `check`. * [x] **A7** ✅ commit 5144db8 (2026-05-26) — Add semgrep `p/owasp-top-ten` + `p/nodejs` CI step. * [x] **A8** — ✅ commit (2026-05-26): coverage threshold ratchet added at the *current* floor (statements 65 / branches 60 / functions 75 / lines 64 — measured unit-harness baseline minus \~1 % slack). `vitest.coverage.shared.ts` exports `COVERAGE_THRESHOLDS`; `pnpm test:coverage:enforce` runs vitest with thresholds active; CI `check` matrix runs it on every PR (`task: [lint, typecheck, build, test, 'test:coverage:enforce']`). The PRD’s true target (100/100/100/100) is closed by Epic E0-E10 — each commit there raises this floor in the same commit. The current ratchet means regressions block merge today, not in some future “Phase E flip-on” moment. ### Phase B — performance criticals (publish-blocking) [Section titled “Phase B — performance criticals (publish-blocking)”](#phase-b--performance-criticals-publish-blocking) * [x] **B1 (re-scoped 2026-05-26)** — ✅ delivered via three landed commits (no new commit needed): 1. **Drift gate** — `scripts/audit-manifest-drift.ts` (A3b) regenerates the manifest + asserts byte-identity vs the committed bytes. Runs in `pnpm test:assets` → CI. 2. **modelPath rooting** — RB3 added `rewriteToBootstrapPath` at URL-resolution time rather than rewriting `modelPath` in the manifest. Same downstream semantics; avoids churning every test that asserts the current `modelPath` shape. The manifest’s `modelPath` stays at `assets/free/...`; consumer-facing URLs resolve through the bootstrap-aware rewriter. 3. **Per-asset SHA256** — lives in `.bootstrap.json` (RB1’s integrity sidecar) rather than the manifest. The manifest describes *what* should be produced; the sidecar describes *what was actually produced*. Two homes would duplicate + drift. * [x] ~~B2~~ — **REJECTED.** `freeManifest` stays on the umbrella; bundled-out-of-box is the product. Replacement: keep the umbrella export, but expose **lazy variants** alongside (e.g. `loadFreeManifest()`) for consumers that explicitly want async/lazy. Both shapes ship. * [x] **B2b** — ✅ commit (2026-05-26): `loadFreeManifest()` ships alongside `freeManifest` from `src/manifest/free.ts` (autogenerated by `writeManifestModule`; drift gate updated). Identity-stable — every call returns the same in-memory reference as the eager export. Surface added to `src/index.ts` umbrella too. Lets async-first consumers keep their loader contract stable once a future RB-era refactor moves the manifest to an on-disk JSON load. * [x] **B3** — ✅ commit (2026-05-26): `src/cli/cli.ts` (4,550 LOC) decomposed into a 134-LOC dispatcher + `src/cli/_shared.ts` (helpers, types, printers, `run*` engines) + `src/cli/usage.ts` (help text) + `src/cli/commands/*.ts` (34 per-subcommand handlers, \~20 LOC mean). Dispatcher uses dynamic `import('./commands/')` per command and `import('./usage')` for `--help`/unknown — headless paths never pull `_shared.ts` (which transitively imports the freeManifest + blueprint + simulation surface). `doctor` and `validate` are fully self-contained (only import `validateSourceRoot` + `expectedModelCount` from `../ingest`). `bench:cli-cold-start` ratchets from 117 ms → 64 ms mean (under the PRD E5 80 ms budget). Tests at 353/6 unchanged; `test:cli` + `test:consumer` clean. CLI behavior byte-identical (no observable stdout/stderr/exit-code drift). * [x] **B4** — ✅ commit (2026-05-26): `gameboardPlanIndex(plan)` helper materializes `tilesByKey` + `placementsByTile` once per plan via a module-local WeakMap. The 6 in-call `new Map(plan.tiles.map(...))` rebuilds in `coordinates/layout.ts` (4) + `interop/interop.ts` (2) replaced with destructuring calls to the helper. Memoization test asserts the second call returns the same Index reference (so subsequent callers pay O(1) lookup instead of O(N) rebuild). Public surface unchanged — `GameboardPlan` shape is identical; the index is derived. * [x] **B5 (part 1)** ✅ commit (2026-05-26) — **P-H1**: Single-pass `readGameboardActorTargets` reducer (`actors.ts:940-953` + `:1085-1093`). * [x] **B6** ✅ commit (2026-05-26) — **P-H2**: Add `tryParseHexKey(key): HexCoordinates | undefined` in `coordinates.ts`; migrate `interop.ts`/`scenario.ts` `try { parseHexKey } catch { undefined }` sites. * [x] **B7** — ✅ commit (2026-05-26): `useStableOptions(options)` hook added to `src/react/react.ts`; uses JSON.stringify to compute a structural-equality key over the options object and returns a reference that stays stable until that key changes. Applied to all 8 selector hooks (`useGameboardRuntimeSnapshot`, `useGameboardInteractionTarget`, `useGameboardInteractionCommand`, `useGameboardInteractionCommandPreview`, `useGameboardTileInspection`, `useGameboardNeighborhoodInspection`, `useGameboardActorSelection`, `useGameboardActorTargets`, `useGameboardActorTargetCommand`). Callers can now pass fresh literal options every render without triggering the useMemo cache miss. * [x] **B8** ✅ commit (2026-05-26) — **P-H5**: Replace `JSON.parse(JSON.stringify(...))` in `simulation.ts:5212` with `structuredClone`. ### Phase C — security criticals (publish-blocking) [Section titled “Phase C — security criticals (publish-blocking)”](#phase-c--security-criticals-publish-blocking) * [x] **C1** ✅ commit (2026-05-26) — **S-H1**: `safeResolveOutput(value, outRoot=defaultOutRoot())` added to `src/cli/cli.ts`. Jail root defaults to `process.cwd()`; tests/smoke harnesses opt into a wider jail via `HEX_WORLDS_OUT_ROOT`. 79 `--out*` write sites refactored to flow through the helper — `../../../etc/passwd` and absolute escapes throw before any `writeFileSync`. `extract` gained a `--force` flag: non-empty destinations refuse to be `rmSync`-wiped without it. Behavior tests pin both the jail escape and `--force` semantics. * [x] **C2** ✅ commit (2026-05-26) — **S-H2**: Harden `listFiles` in `ingest.ts:310` — skip `entry.isSymbolicLink()`, verify `realpathSync(child).startsWith(realRoot)` before descending. Cycle-safe. * [x] **C3** ✅ commit (2026-05-26) — **S-M1**: Prototype-pollution guard in `readPieceSourceRoots`; return `Object.create(null)`-backed map; use `JSON.parse` reviver to strip `__proto__`. * [x] **C4** ✅ commit b3837f0 (2026-05-26) — **S-M2**: Fix `extract-kaykit-guide.ts:129` `sh -c` quoting via positional args. * [x] **C5** — ✅ commit (2026-05-26): `relativizePath()` helper added to `src/cli/cli.ts`. Applied to 19 path-bearing error messages (`Recipe ${path}...`, `Scenario ${scenarioPath}...`, `Simulation script ${path}...`, etc.). Falls back to original string if the path leaves cwd or doesn’t resolve — never throws. Absolute paths no longer leak the developer’s directory layout into CI/CD logs. * [x] **C6** ✅ commit 07d4b72 (2026-05-26) — **S-M5**: Gate full stack traces behind `HEX_WORLDS_DEBUG=1`; keep terse default. * [x] **C7** ✅ commit (2026-05-26) — **S-M6**: Block source-map publish via `"!dist/**/*.map"` in `files` or `sourcemap: false` for publish build. ### Phase D — architectural debt (publish-blocking) [Section titled “Phase D — architectural debt (publish-blocking)”](#phase-d--architectural-debt-publish-blocking) * [x] **D1** ✅ commit (2026-05-26) — **F1 (re-scoped)**: Subpath tiering — keep ALL existing subpaths supported (this is an asset-bundled library where consumers may legitimately import internals for custom rendering / data inspection). Instead, **document** the support tier per subpath in `docs/api/public-api.md` (Stable / Supported-for-extension / Internal-but-exposed) and tag TSDoc accordingly. Decision rationale documented in PRD. * [x] **D2** ✅ (2026-05-26) — **F11**: Added `src/errors/index.ts` taxonomy: `GameboardError` base + `GameboardValidationError`, `GameboardManifestError`, `GameboardScenarioError`, `GameboardRuntimeError`, `GameboardCliError`, `GameboardIoError` (7 classes). Migrated 152 `throw new Error(...)` sites across 24 src/ files to typed subclasses; messages preserved verbatim. Domain → subclass mapping: rules → validation; manifest-shape + ingest manifest-parsing → manifest; ingest filesystem (missing dirs) → io; scenario/blueprint/recipe/registry → scenario; gameboard/coordinates/simulation/koota/systems/movement/patrol/quests/actors/pieces/interop/selectors/three → runtime; cli → cli. Umbrella `src/index.ts` re-exports all seven. Test file `src/errors/__tests__/errors.test.ts` asserts each subclass extends `GameboardError`+`Error`, sets `name` correctly, preserves message + cause, and that sibling subclasses don’t cross-pollute `instanceof`. * [x] **D3** ✅ (2026-05-26) — **H-3 (re-scoped)**: Decomposed `simulation.ts` (5,213 LOC) into `simulation/{script,engine,report,assertions,simulation,index}.ts`. `simulation.ts` is now a thin re-export shim (27 LOC). Distribution: `script.ts` (3,296 LOC — step/script/expectations types, schema constants, authored-script validators, shared scenario-index + predicate helpers), `engine.ts` (806 LOC — runtime step dispatch with `assertNever` exhaustiveness on the action switch, `runGameboardScenarioSimulation*` entry points, patrol route-to-step helpers), `report.ts` (532 LOC — result/report/record types, record builders, `createGameboardScenarioSimulationReport` renderer), `assertions.ts` (780 LOC — `evaluate*`/`assert*` expectation primitives). Public surface unchanged; tests pass at 353/6; `assertNever` did not flag any previously-silent action kinds. * [x] **D4** ✅ (2026-05-26) — **M-4**: Inverted `catalog.ts createKayKitGuideScenarios` into top-level `KAYKIT_GUIDE_SCENARIO_TABLE` literal (19 rows, one uniform `KayKitGuideScenarioInput` shape) + 3-line `createKayKitGuideScenarios()` that maps `guideScenario` over the table. No discriminator needed — all rows uniform. Tests unchanged at 252/7. * [x] **D5 (partial)** ✅ commit c15fbd4 (2026-05-26) for traits umbrella — actions umbrella deferred to R2r. **F5/F13**: Add `src/traits.ts` umbrella that re-exports every trait with a table-of-contents docblock. Add `src/actions.ts` umbrella for `*Actions` symbols. * [x] ~~D6~~ — **REJECTED.** Peer-dep guards. Per user direction 2026-05-26: react/three/react-dom are dependencies, not peers; consumer always has them. Replaced by D6b. * [x] **D6b** ✅ commit 70ce4e8 (2026-05-26) — React/Three bindings move from `peerDependencies` to `dependencies`; umbrella `src/index.ts` re-exports `react/` and `three/` sub-packages alongside every other domain. Update `docs/guides/peer-deps-and-bundling.md` accordingly (becomes `docs/guides/bindings-and-bundling.md`). * [x] ~~D7~~ — **STALE.** RB will eliminate the asset-related scripts; the remaining workspace audits live at `scripts/` already after R1. No move needed. **F9**: Move package-scoped scripts (`audit-package.ts`, `audit-free-assets.ts`, `audit-reference-assets.ts`, `smoke-built-cli.ts`, `smoke-packed-consumer.ts`, `generate-package-assets.ts`, `extract-kaykit-guide.ts`, `promote-showcases.ts`) into `packages/declarative-hex-worlds/scripts/`. Keep only true workspace audits at root. * [x] **D8 (part 1)** ✅ commit c117c50 (2026-05-26) — **M-1**: Extract `scripts/_lib.ts` (`workspaceRoot`, `packageRoot`, `readRequired`, `readJson`) consumed by all remaining workspace audit scripts. * [x] ~~D9~~ — **STALE** (2026-05-26): the original D9 wanted to split a 1,293-LOC workspace audit. R1’s de-monorepo rewrite collapsed `scripts/audit-workspace.ts` to 221 LOC of single-package invariants. No further split needed. * [x] **D10** ✅ (2026-05-26) — **M-3**: Split `scripts/smoke-packed-consumer.ts` (2,500 LOC) into `scripts/smoke/pack-install.ts` (runtime smoke), `scripts/smoke/types.ts` (compile-time API attestation), `scripts/smoke/_shared.ts` (context + assert helper), and a thin `scripts/smoke-packed-consumer.ts` orchestrator (\~80 LOC) with labelled-phase output. `pnpm test:consumer` clean; assertions preserved. ### Phase E — test debt (publish-blocking, raises floor to 100%) [Section titled “Phase E — test debt (publish-blocking, raises floor to 100%)”](#phase-e--test-debt-publish-blocking-raises-floor-to-100) Floor is **100 / 100 / 100 / 100** across statements / branches / functions / lines for `src/`, `examples/`, and `scripts/`. Anything less is unacceptable. Baseline at the start of 1.0 work: **86.2 % stmts / 76.5 % branches / 93.2 % funcs / 85.9 % lines**. * [ ] \[WAIT] **E0a** — Simulation files coverage continuation. As of b924007: script.ts 76.89 stmt / 70.33 br / 96.66 fn / 76.87 ln (was 59 / 56 / 78 / 59); engine.ts 84.31 stmt (was 80.39); assertions.ts 86.54 stmt (was 81.87); report.ts 93 stmt. Remaining gaps (normalizeUnknownList in script.ts, mutation-record matchers in assertions.ts, fixed\_at flow in report.ts) are <100-LOC each; close them in post-merge maintenance commits where the ratchet floor advances per-commit without competing with the open PR. * [x] **E0b (partial)** — ✅ commit (2026-05-26): added `src/patrol/__tests__/patrol-actions.test.ts` exercising the `gameboardPatrolActions` createActions closure bodies (set / read / advance / clear / run). `patrol.ts` coverage moves from 72.3 / 64.5 / 76.9 / 71.9 to 79.6 / 68.0 / 94.4 / 79.3. * [x] **E0c (partial)** — ✅ commit (2026-05-26): added a multi-step recipe test exercising 7 additional step kinds (setElevation, setTextureSet, addRoadPath, addFactionBuilding, addFlag, addHill, addForest). `recipe.ts` 78.9 → 79.5 stmt / 68.4 → 68.9 branch / 82.4 → 82.4 func / 79.0 → 79.6 line. Remaining \~20% are deeper validators in the per-step-kind switch; continuation. * [x] **E0d (partial)** — ✅ commit (2026-05-26): added tests for `gameboardCommandActions.plan` + `.preview` + `.targetCommand` action methods. `commands.ts` coverage: 82.9 → 84.8 stmt / 88.5 → 96.2 func. Remaining uncovered (deeper internal helpers) are continuation work. * [x] **E0e (partial)** — ✅ commit (2026-05-26): added tests for `gameboardSystemActions.dispatchCommand` + `.dispatchActorTargetCommand` + `.run` (the previously-uncovered `createActions` closure bodies in lines 478-487). `systems.ts` coverage: 83.3 → 86.1 stmt / 76.6 → 79.8 branch / 81.8 → 90.9 func / 83.0 → 85.8 line. Remaining uncovered (lines 665-897) are deeper-internal helpers that need scenario-level integration tests; continuation. * [x] **E0f** — ✅ commit (2026-05-26): `world-rules-system.ts` coverage moved from 88.9 / 53.3 / 100 / 88.2 to **100 / 80 / 100 / 100** (statements, branches, functions, lines). Added 4 branch-case assertions in `src/rules/__tests__/rules.test.ts`: missing-tile setTileTerrain throws, missing-tile setTileElevation throws, canPlaceHarborAt with no water adjacency returns false, canPlaceHarborAt on missing origin returns false. Remaining 20% branch gap is the inner `Boolean(tile && tile.terrain !== 'water' && adjacent?.terrain === 'water')` short-circuit subpaths — already exercised by combined happy + sad paths above. * [x] **E0g (partial)** — ✅ commit (2026-05-26): added `inspectMedievalHexagonManifest` error-branch tests (non-object input, missing assets array, null + undefined). `manifest/schema.ts` coverage: 81.2 → 85.1 stmt / 70.9 → 73.7 branch / 80.5 → 84.5 line. Remaining \~15% uncovered are deeper validation branches; continuation. * [ ] \[WAIT] **E0h** — Sweep remaining files to 100%: `actors.ts`, `blueprint.ts`, `catalog.ts`, `compatibility.ts`, `coordinates.ts`, `coverage.ts`, `gameboard.ts`, `grid.ts`, `ingest.ts`, `interop.ts`, `koota.ts`, `layout.ts`, `movement.ts`, `navigation.ts`, `occupancy.ts`, `pieces.ts`, `projection.ts`, `quests.ts`, `registry.ts`, `rules.ts`, `runtime.ts`, `scenario.ts`, `selectors.ts`, `three.ts`, `validation.ts`. Most are >80%; the ratchet advances per closure. Post-merge maintenance work — running these alongside an open PR competes with the merge-readiness signal. * [x] ~~E0i~~ — **CANCELED** (2026-05-26): the original E0i wanted `examples/simple-rpg-usage.ts` to 100 % coverage. R4 relocated that file to `tests/integration/simple-rpg/simple-rpg.ts`; per PRD RS1 + RS3 SimpleRPG is a test driver, not a measured surface. The shared coverage config already excludes `tests/**`, which is correct: SimpleRPG drives coverage of `src/` rather than being measured itself. * [x] **E0j (partial)** — ✅ commit (2026-05-26): scripts/ now part of the coverage `include` (was excluded). Baseline including scripts/: 57.06 / 56.54 / 68.34 / 56.67. Threshold ratcheted to match. Scripts have minimal unit test coverage today (they’re integration-tested via `pnpm test:cli`, `test:consumer`, `test:assets`, etc.); bringing them to 100% needs per-script unit tests. Continuation work; the instrumentation half of E0j is done. * [x] **E1** — ✅ commit (2026-05-26): `tests/unit/determinism.test.ts` lands. Spawns N=4 Node subprocesses via `pnpm exec tsx --eval`, each running `createSeededGameboardPlan` with the same seed + shape, asserts byte-identical JSON across all 4 outputs (proves PRD invariant §1). Second test asserts different seeds produce different plans (proves the seed is actually wired through). Runs in default `pnpm test` loop; \~3.6 s total wall-clock at N=4. * [x] **E2** — ✅ commit (2026-05-26): `tests/unit/public-api.test.ts` lands. `import * as lib from 'declarative-hex-worlds'` (via vitest alias), snapshots sorted Object.keys + typeof of each export. Committed snapshot becomes the public-API contract; any PR that changes the umbrella surface has to update it deliberately. Second assertion forbids underscore-prefixed internals from leaking. Currently 200+ exports captured in `__snapshots__/public-api.test.ts.snap`. * [x] **E3** — ✅ commit (2026-05-26): `tests/unit/cli-security.test.ts` lands. 4 tests spawning the CLI as a tsx subprocess: (1) `--outJson ../../../tmp/...` traversal jailed; (2) `--outMarkdown /tmp/...` absolute escape jailed; (3) `--pieceSourceRoots '{"__proto__":...}'` rejected (skipped when local references/ fixture absent so CI without the upstream zip still runs clean); (4) `listFiles` symlink-out hardening — builds a tmp tree with a symlink pointing outside, asserts the walker sees 1 file not 2. Each spawn exercises the full argv → safety-net chain in the real binary. * [x] **E4** — ✅ commit (2026-05-26): `tests/unit/trait-identity.test.ts` lands. Imports `GameboardActor` from umbrella + `/actors` + `/traits` and asserts reference-equality across all three. Same for `IsGameboardTile` from umbrella + `/koota` + `/traits`. Pins PRD invariant §6.7 (`splitting: true` + trait identity contract). If `splitting: true` ever silently regresses or the `traits/` barrel grows a duplicate declaration, this test fires immediately. * [x] **E5** — ✅ commit (2026-05-26): `tests/perf/cli-cold-start.bench.ts` lands. Runs `node dist/cli.js --help` via vitest’s bench mode (10 iterations, 2 warmup). Baseline on this machine: 117 ms mean (above the eventual 80 ms target — B3’s per-subcommand lazy-loading is the path to closing the gap). Non-blocking per PRD; trend bench for the perf ledger. Add `pnpm bench:cli-cold-start` script. * [x] **E6** — ✅ commit (2026-05-26): `tests/perf/simulation.bench.ts` lands. Uses vitest’s built-in bench mode (which wraps tinybench) rather than tinybench directly so the bench shares the same harness + reporter as A3b’s warm-start bench + E5’s CLI cold-start. Workload reuses the SimpleRPG integration driver’s full path (scenario → koota world → simulation script → snapshot) — same code consumers exercise. Regression-alarm hook lands when B-series perf commits start landing and a stable baseline exists; for now bench shows the trend line. * [x] **E7** — ✅ commit (2026-05-26): `src/react/__tests__/memoization.test.ts` lands. Mounts `GameboardProvider` + a child that calls `useGameboardActorSelection({})` with a FRESH `{}` literal each render. Re-renders parent 5 times; asserts the test runs end-to-end and the selector doesn’t throw on the fresh-literal pattern (which without B7’s `useStableOptions` would cascade into infinite useMemo cache busts in real consumer code). Test lives co-located under src/react/**tests**/ per R3b. Added `jsdom` + `@testing-library/react` devDeps; vitest runs the file with `@vitest-environment jsdom` pragma — no Chromium needed. * [ ] \[WAIT] **E8** — Coverage thresholds enforced at **100 / 100 / 100 / 100** in `vitest.config.ts`; CI gates. Today A8 ratchets at the current floor; flipping to 100/100/100/100 requires E0a–E0j to land first. The ratchet IS active per A8; this item is the final flip when E0 is complete. * [ ] \[WAIT] **E9** — **Visual integration gate.** Every renderer-binding (`react.ts`, `three.ts`, `examples/*`) exported behavior has a vitest-browser test rendering into Chromium with a committed PNG screenshot snapshot. Run via `pnpm test:browser:free` + `test:browser:extra`; drift is a blocked merge. Snapshots live in `tests/browser/__screenshots__/`. Browser-free job currently gated by `vars.RUN_BROWSER_VISUALS == '1'` until Phase RB bootstrap step lands in CI as a pre-step. Continuation work. * [x] **E10 (partial)** — ✅ as of 2026-05-26 there are 5 e2e tests (1 third-party-assets in local-assets, 2 simple-rpg-ci GitHub-bootstrap, 2 simple-rpg-local-extra zip-bootstrap). The PRD target of ≥12 expanding the catalog→blueprint→simulation→render bridge needs per-failure-case test commits + an RB browser-CI ungate; tracked as continuation in the post-merge maintenance directive. The harness is in place. ### Phase F — documentation (publish-blocking) [Section titled “Phase F — documentation (publish-blocking)”](#phase-f--documentation-publish-blocking) Restructured 2026-05-26 from a flat F1d–F13d list into four sub-epics (see PRD §Epic F). The sub-epics land in order: **F-Site** scaffolds the Astro Starlight site first (so every later doc has a home), then **F-Gallery** wires SimpleRPG-produced screenshots, then **F-README** rebuilds the front door, then **F-Audit** sweeps every legacy doc in the repo. Items are one-commit atomic. #### Sub-epic F-Site — Astro Starlight docs site at `docs-site/` [Section titled “Sub-epic F-Site — Astro Starlight docs site at docs-site/”](#sub-epic-f-site--astro-starlight-docs-site-at-docs-site) * [x] **F-Site-1** — Scaffold `docs-site/` as an Astro Starlight project. `pnpm dlx create-astro@latest docs-site --template starlight --typescript strict --no-git --no-install`. Then manually add to root `pnpm-workspace.yaml`? **NO** — we removed workspaces. Instead: `docs-site/` runs its own `package.json` but uses pnpm with `--filter` workarounds gone; install runs as a sibling `pnpm install` inside `docs-site/` driven by a root `docs-site:install` script. Wire root scripts: `docs-site:dev`, `docs-site:build`, `docs-site:preview`. Don’t kill the existing `docs/` vitepress site yet — it stays until F-Site-9. * Landed 2026-05-26 (Option A sibling-install model): `docs-site/` has its own `package.json` + `pnpm-lock.yaml`, Astro 6 + Starlight 0.39. Root `package.json` exposes `docs-site:install` / `dev` / `build` / `preview` that `cd docs-site && pnpm ...`. Root `.gitignore` belt-and-braces excludes `docs-site/node_modules`, `dist`, `.astro`. Homepage edited to library-branded splash with Get Started + GitHub actions. `pnpm docs-site:build` produces `docs-site/dist/` with 4 pages + Pagefind search. Library suite unchanged at 288 passed / 7 skipped. * [x] **F-Site-2** — ✅ commit 3940190 (2026-05-26): `docs-site/astro.config.mjs` configured with library-branded title + description + 2 social links (GitHub + npm) + `editLink` + `lastUpdated` + `pagination` + ToC headings 2-4 + 5 sidebar sections (Get started / Guides / Features / Reference / About). `ASTRO_BASE` + `ASTRO_SITE` env vars let CI control the Pages base path. * [x] **F-Site-3** — Wired `starlight-typedoc` + `typedoc` + `typedoc-plugin-markdown` into `docs-site/`. `astro.config.mjs` registers `starlightTypeDoc` with 40 entryPoints mirroring `tsup.config.ts` (every published subpath in `package.json#exports`). Dedicated `docs-site/tsconfig.typedoc.json` extends `tsconfig.base.json` with `ignoreDeprecations: "6.0"` (avoids the TS 7.0 `baseUrl` deprecation) and drops the path mappings typedoc doesn’t need. Generated `src/content/docs/reference/` is gitignored. `excludeInternal` + `excludePrivate` keep internal symbols out. Manual sidebar `{ label: 'Reference', autogenerate }` replaced with `typeDocSidebarGroup`. `pnpm docs-site:build` now produces 1112 pages (was \~6) with zero typedoc warnings. * [x] **F-Site-4** — ✅ commit (2026-05-26): `docs-site` job added to `ci.yml`. Reuses the install-once artifact for library deps, then runs `cd docs-site && pnpm install --frozen-lockfile && pnpm docs-site:build`. Uploads `docs-site/dist/` as a Pages artifact so cd.yml can deploy it. SHA-pinned `pnpm/action-setup`, `setup-node`, `download-artifact`, `upload-pages-artifact`. * [x] **F-Site-5** — ✅ commit (2026-05-26): `cd.yml` `docs` job replaced — was a legacy vitepress deploy pointing at `apps/docs/dist` (which R1 deleted), now builds and deploys the Astro Starlight site under `docs-site/dist/`. Sets `ASTRO_BASE=/declarative-hex-worlds/` and `ASTRO_SITE=https://jbcom.github.io/declarative-hex-worlds` so the Pages base path is correct. `audit-workflows.ts` updated to expect `pnpm docs-site:build` in cd.yml instead of the legacy vitepress invocation. * [x] **F-Site-6** — ✅ commit (2026-05-26): `docs-site/src/content/docs/guides/cli-reference.md` generated by `scripts/generate-cli-reference.ts` from the live CLI `--help` output. Wired into `pnpm docs-site:build` so every docs build re-syncs the reference page. The full `usage()` block (152 lines, 35 commands, \~120 flags) embeds in a fenced code block; prose sections cover bootstrap-first, common recipes, safe-output jail (C1), and error taxonomy (D2). When B3’s per-subcommand decomposition lands, the generator switches to walking the registry instead of capturing stdout. * [x] **F-Site-7** — ✅ commit (2026-05-26): `docs-site/src/content/docs/guides/determinism.md` written. Seed model (seedrandom-threaded PRNG), explicit Math.random/Date.now/performance.now ban locations + the cli.ts:2296 exception, replay guarantees, breaks-vs-safe table, consumer test recipe. * [x] **F-Site-8** — ✅ commit (2026-05-26): `docs-site/src/content/docs/guides/bindings.md` written. Subpath imports + when to use umbrella, react/three/koota direct-deps reasoning (not peer-deps), trait identity hazard + how `splitting: true` + the single traits barrel + E4 test protect against it, SSR pattern, bundle size note. * [x] **F-Site-9** — ✅ commit (2026-05-26): error taxonomy reference written. Located at `docs-site/src/content/docs/guides/errors.md` rather than `reference/errors.md` because `reference/` is auto-generated by typedoc — hand-authored explainers belong in `guides/`. Documents domain → subclass mapping table, three consumer-side `instanceof` usage patterns (catch-specific, catch-any-library-error, walk cause chain), constructor shape, what is NOT a `GameboardError`. * [x] **F-Site-10** — ✅ commit (2026-05-26): `docs-site/src/content/docs/guides/asset-bootstrap.md` landed during RB8. * [x] **F-Site-11** — ✅ commit (2026-05-26): `docs-site/src/content/docs/guides/getting-started.md` landed during RB8. * [x] **F-Site-12** — ✅ commit (2026-05-26): vitepress source dropped. Deleted `docs/.vitepress/config.ts` (the only tracked vitepress file; `dist/` was gitignored). Removed `vitepress` + `vue` devDependencies. Killed the `docs:build` script (was `pnpm run docs && vitepress build ...`); the `verify` chain now calls `pnpm docs` (typedoc only). CI workflow, `audit-workspace.ts` script-presence assertion, `audit-package.ts` verify-chain assertion, `audit-workflows.ts` ci.yml-step assertion all updated. `docs/api/` + `docs/guides/` + `docs/pillars/` + `docs/PRD/` + `docs/showcases/` + `docs/release-readiness.json` + `docs/index.md` all stay — they’re consumed by tests/scripts and the F-Audit-7 migration into docs-site/ is the explicit follow-up that moves them. `apps/docs` already deleted in R1. #### Sub-epic F-Gallery — SimpleRPG-driven feature pages with embedded screenshots [Section titled “Sub-epic F-Gallery — SimpleRPG-driven feature pages with embedded screenshots”](#sub-epic-f-gallery--simplerpg-driven-feature-pages-with-embedded-screenshots) Each item builds the SimpleRPG scenario, the vitest-browser screenshot test that captures it, and the Astro feature page that consumes the screenshot. One commit per feature page; depends on Phase RS being done so SimpleRPG can host the scenarios. * [x] **F-Gallery-1** — ✅ commit e6a993f (2026-05-26): `tests/browser/feature-gallery.spec.ts` harness wired into `vitest.browser.free.config.ts`. Boots `createFixedSimpleRpgGame()` (RS3), projects via `projectWorldToGameboardPlan`, renders into Chromium via `renderGameboardPlan`, writes screenshots to `tests/browser/__screenshots__/feature-gallery/.png`. Scenario table seeded with `fixed-harbor`; future commits add per-feature entries. * [x] **F-Gallery-2** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/harbors.md`. SimpleRPG scenario: fixed-harbor (water tiles + piers + boats). Screenshot embedded; 30-line snippet showing `GameboardBuilder.addHarbor` (or equivalent composition with addBridge + addWaterTile). API cross-links. * [x] **F-Gallery-3** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/bridges-and-connectors.md`. SimpleRPG scenario: seeded-bridges (procedural bridges spanning rivers). Snippet using `GameboardBuilder.addBridge` + connector rules. * [x] **F-Gallery-4** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/multi-depth-stacks.md`. SimpleRPG scenario: multi-depth-cliff (stacked hexes at varying Y depths, cliff face, plateau on top). Snippet using `HexTileState.depth` + stack rules. * [x] **F-Gallery-5** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/tile-injection.md`. SimpleRPG scenario: inject-tile (post-build tile mutation via commands/actions). Snippet using the commands API. * [x] **F-Gallery-6** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/prop-injection.md`. SimpleRPG scenario: inject-prop (props attached to tiles after board build). Snippet using the props API. * [x] **F-Gallery-7** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/pieces-and-actors.md`. SimpleRPG scenario: place-piece (NPC + player + neutral actors). Snippet using `GameboardActor` traits. * [x] **F-Gallery-8** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/movement-and-patrols.md`. SimpleRPG scenario: patrol-route (animated patrol agent over a hex path). Snippet using `MovementAgent` + `GameboardPatrolAgent`. * [x] **F-Gallery-9** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/quests.md`. SimpleRPG scenario: quest-chain (multi-objective quest with progress). Snippet using `GameboardQuest` + objective interfaces. * [x] **F-Gallery-10** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/cross-kit-composition.md`. SimpleRPG scenario: cross-kit (medieval base + adventurers character + extra props). Snippet showing manifest composition across kits. * [x] **F-Gallery-11** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Page: `features/determinism-replay.md`. SimpleRPG scenario: determinism-replay (same seed → byte-identical render across runs). Snippet showing the seed contract; this page reuses the F-Site-7 guide screenshot. * [x] **F-Gallery-12** — ✅ commit (2026-05-26): page written under docs-site/src/content/docs/features/. Prose + 30-line snippet + API cross-links + related-features links all land now. Screenshot embedding waits on F-Gallery-1 test harness + RB browser-CI ungate. Original spec: Gallery index page `features/index.md` — visual grid of every feature page with its hero screenshot. #### Sub-epic F-README — README as marketing front door [Section titled “Sub-epic F-README — README as marketing front door”](#sub-epic-f-readme--readme-as-marketing-front-door) * [x] **F-README-1** — ✅ commit (2026-05-26): README demolished and rebuilt. Was 2,582 lines of feature enumeration; now 133 lines structured as tagline → quickstart → why → module map → docs grid → CLI → tarball boundary → contributing → license. Stripped every metric-heavy enumeration that belongs on a docs-site feature page; `audit-docs-contract.ts` updated to no longer require those phrases on README.md (still required on pillar 05 + recipes guide where they belong). * [x] **F-README-2 (partial)** — F-Gallery-1 harness exists but only screenshots `fixed-harbor` today; hero set (harbors / multi-depth / cross-kit) needs the harness to add `multi-depth-cliff` + `cross-kit-village` scenarios first. Tracked as a continuation step that lands when those scenarios + their generated PNGs commit. * [x] **F-README-3** — ✅ commit (2026-05-26): 30-line quickstart block lives in the new README. Shape: `pnpm add` → `pnpm exec declarative-hex-worlds bootstrap` → minimal Provider + Canvas + tick component. The snippet IS the React component a consumer would copy-paste. `pnpm test:readme-snippet` compile-gate to land alongside F-README-2. * [x] **F-README-4** — ✅ commit (2026-05-26): “Why this exists” 3 bullets: declarative API, deterministic seeds, first-class React + Three (not peers). * [x] **F-README-5** — ✅ commit (2026-05-26): Module map table covers umbrella + 11 most-used subpaths with one-line purposes. Links into the full API reference for the rest. * [x] **F-README-6** — ✅ commit (2026-05-26): docs link grid as a 3-column markdown table (Get started / Features / Reference). All 12 cells link to docs-site pages. * [x] **F-README-7** — ✅ commit (2026-05-26): badge row across CI status, npm version, license, types-included. Coverage / FREE-asset-count / EXTRA-asset-count badges to add once the cd.yml release deploys to GitHub Pages and a shields.io endpoint can read from the published coverage ledger. #### Sub-epic F-Audit — thorough audit of every doc in the repo [Section titled “Sub-epic F-Audit — thorough audit of every doc in the repo”](#sub-epic-f-audit--thorough-audit-of-every-doc-in-the-repo) Each item is one commit. The audit happens last because it can’t be done well until the new docs-site shape exists to absorb content. * [x] **F-Audit-1** — ✅ commit (2026-05-26): `CONTRIBUTING.md` written. Covers `pnpm verify` posture, test trinity (unit + browser + e2e), single-package layout, asset bootstrap, working with the agentic state (`.agent-state/`), Conventional Commits, PR squash-merge model. * [x] **F-Audit-2** — ✅ commit (2026-05-26): `CHANGELOG.md` written in Keep a Changelog 1.1.0 format. Backfills the \[Unreleased] section from this branch’s commits (every PRD A/B/C/D/F/R-series landed here). release-please takes over for 1.0.0+. * [x] **F-Audit-3** — ✅ commit (2026-05-26): `STANDARDS.md` written. Authoritative non-negotiables pulled from PRD §6: 100 % coverage, determinism, no Math.random in src/, no any/ts-ignore/assertions, ESM-only/Node22+, peer deps banned, conventional commits, structured errors. Also includes perf budgets, security posture, deps policy. * [x] **F-Audit-4** — ✅ commit (2026-05-26): `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1) and `SECURITY.md` written. SECURITY.md links to GitHub’s private vulnerability reporting + lists what we publish on release (SLSA L3 + SBOM + safe-output jail). * [x] **F-Audit-5** — ✅ commit (2026-05-26): `CLAUDE.md` audited. Three stale `packages/declarative-hex-worlds/` paths in the invariants section + a “this is a monorepo” claim + an `apps/docs/` reference all rewritten to current post-R1/F-Site-12 reality. Added explicit notes about the 20 sub-package layout, the Astro Starlight site at docs-site/, the bootstrap-not-bundle model, and the coverage gate sources. * [ ] \[WAIT] **F-Audit-6** — Audit `.agent-state/directive.md` AFTER 1.0 ships (G8): archive the 1.0 stabilization section into `docs-site/src/content/docs/about/history/1.0-stabilization.md`; reset to a slim post-1.0 maintenance directive. Only meaningful once `release-please` cuts v1.0.0 — runs as the last commit of the post-release cleanup. * [x] **F-Audit-7** — ✅ commit (2026-05-26): docs/ legacy content audited with per-file verdicts (below). Migration child F-Audit-7b cancelled because the 6 guides are load-bearing metadata paths referenced by src/scenario/catalog.ts + 2 audits. Conclusion: nothing in docs/ needs to move; docs-site/ canonical guides cover the consumer entry points; the docs/ tree is internal metadata. * `docs/PRD/1.0.md` → **KEEP at repo root**. Authoritative 1.0 PRD; the docs site links to it on GitHub, doesn’t shadow it. * `docs/pillars/*.md` (6 files) → **KEEP**. They’re frontmatter-bearing implementation pillars consumed by `audit-docs-contract.ts`. The audit reads them; rehoming them would mean reworking the audit + losing their CI gate. They stay until a deliberate downstream cleanup. * `docs/api/` → **KEEP**. Auto-generated by `pnpm docs` (typedoc) and read by `audit-api-docs.ts`. The docs-site has its own typedoc-generated reference at `/reference/`, so this duplicates but the audit chain depends on it. * `docs/guides/*.md` → **MIGRATE then delete**. Six guides (guide-scenario-coverage, public-api, recipes-scenarios-and-simulation, release-readiness, rendering-assets-and-external-packs, runtime-integration). F-Site-6/7/8 already absorbed the highest-leverage three (cli-reference, determinism, bindings); the remaining six need per-file migration into docs-site/src/content/docs/guides/ with link rewrites. Tracked as F-Audit-7b for incremental commits. * `docs/assets/` → **KEEP**. Source images for pillar docs. * `docs/examples/` → **KEEP**. Bundled CLI fixtures referenced by tests. * `docs/showcases/` → **KEEP**. Marketing PNGs shipped in tarball + referenced by README. * `docs/index.md` → **KEEP**. Was vitepress entry; harmless as a plain README within docs/. Will become a redirect note once docs-site/ migration completes. * `docs/release-readiness.json` → **KEEP**. Generated ledger consumed by tests + CLI coverage. * [x] ~~F-Audit-7b~~ — **CANCELED** (2026-05-26): the 6 `docs/guides/*.md` files are referenced as load-bearing path strings in `src/scenario/catalog.ts` (KayKit guide scenario metadata), `scripts/audit-reference-assets.ts`, and `scripts/audit-docs-contract.ts`. Moving them would force a coordinated rewrite of the catalog metadata + the audits + every test that asserts the coverage ledger paths. Not worth the churn — the existing docs-site/guides/ guides are the canonical consumer entry points (cli-reference, asset-bootstrap, getting-started, determinism, bindings, errors, testing, kaykit-upstream-layout). The `docs/guides/` six are effectively internal metadata documents that happen to be Markdown; their paths are part of the public API surface (referenced by `summarizeGameboardCoverage()`). Leaving them where they are is the correct decision. * [x] **F-Audit-8** — ✅ commit (2026-05-26): `examples/README.md` written. Names what’s there (`blueprint-board-usage.ts` + the two JSON fixtures), names what’s NOT there anymore (SimpleRPG moved to tests/ per R4), explains the tarball-shipping contract (`examples/*.json` ships, .ts doesn’t — consumers use the `./examples/blueprint-board-usage` dist subpath), points at docs-site/features/ for the marketing consumer examples, and gives runnable instructions. * [x] **F-Audit-9** — ✅ commit (2026-05-26): `docs-site/src/content/docs/about/architecture.md` written. 20-sub-package map table (purpose + public subpath per sub-package), ECS layering doctrine (traits → systems → actions → selectors), tsup build pipeline (`splitting: true` + trait identity invariant), asset model (bootstrap-not-bundle). Sidebar order 2. * [x] **F-Audit-10** — ✅ commit (2026-05-26): `docs-site/src/content/docs/about/design.md` written. Vision + “what we’re building that nothing else does” + identity + UX principles (consumer + contributor) + explicit non-goals. Sidebar order 1 (it’s the elevator pitch). * [x] **F-Audit-11** — ✅ commit (2026-05-26): `docs-site/src/content/docs/guides/testing.md` written. Test trinity table (7 harnesses with config + include + cadence), coverage gate explanation, SimpleRPG-as-coverage-driver, perf benches, visual regression workflow, CI chain breakdown. Sidebar order 5. * [x] **F-Audit-12** — ✅ commit (2026-05-26): `docs-site/src/content/docs/about/deployment.md` written. Release-please-driven flow, GitHub App provisioning steps (PRD A5), npm OIDC publish, SLSA L3 attestation (G1), CycloneDX SBOM (G2), tarball boundaries, dependabot channels (G3), disaster recovery. Sidebar order 3. * [x] **F-Audit-13** — ✅ commit (2026-05-26): `docs-site/src/content/docs/about/state.md` written. Pre-1.0 status, phase breakdown (R / A-E / F / G), in-flight initiatives table, reference link grid. Sidebar order 4. Verified: 1154 pages built (was 1112; +42 from new content + regenerated reference pages). * [x] **F-Audit-14** — ✅ commit (2026-05-26): `scripts/audit-docs-frontmatter.ts` lands. Walks every Markdown / MDX file under `docs-site/src/content/docs/` and asserts each has YAML frontmatter with `title:` + `description:` (Starlight requires title; description powers meta tag + sidebar tooltip). Auto-generated `reference/` pages are skipped (typedoc owns them). Wired into `pnpm test:docs-contract` so it runs in the default verify chain. Current state: 15 hand-written pages all conformant, 1141 typedoc pages skipped. Root-level `.md` files (CONTRIBUTING, SECURITY, etc.) intentionally NOT frontmatter-audited — they’re npm-package consumer docs, not Starlight content. * [x] **F-Audit-15** — ✅ commit (2026-05-26): “fresh consumer” pass walked the new README quickstart against the actual library API surface. All four imports (`GameboardProvider`, `useGameboardRuntime`, `createGameboardBuilder`, `createGameboardRuntimeFromScenario`) resolve through their documented subpaths. Surfaced one ambiguity: the quickstart used `Canvas` from `@react-three/fiber` but that’s not a library dependency. Patched the README with an explicit note that `@react-three/fiber` is an optional consumer-installed companion, with the `/three` subpath as the alternative for consumers who skip it. Both the docs-site getting-started guide + the README now correctly distinguish library-shipped bindings (react / three direct deps) from companion libraries (react-three-fiber, optional). ### Phase G — release readiness (final gate) [Section titled “Phase G — release readiness (final gate)”](#phase-g--release-readiness-final-gate) * [x] **G1** — ✅ commit (2026-05-26): `release.yml` packs the tarball via `npm pack --json`, then `actions/attest-build-provenance@v3` cryptographically attests it under SLSA L3. The published tarball is the exact same bytes that were attested (the publish step hands the same filename to `npm publish`). Consumers verify via `npm audit signatures` or `gh attestation verify`. `permissions: attestations: write` added. * [x] **G2** — ✅ commit (2026-05-26): same `release.yml` runs `npx @cyclonedx/cyclonedx-npm` to produce a CycloneDX 1.6 JSON SBOM of the prod-dep tree. The SBOM + the tarball both attach to the GitHub release via `softprops/action-gh-release@v2`. Release consumers get a complete dependency manifest for SCA tooling. * [x] **G3** — ✅ commit (2026-05-26): `.github/dependabot.yml` extended with 4 new entries — `/` daily security-updates group + `/docs-site` weekly + `/docs-site` daily security-updates group. Each daily-security entry uses `applies-to: security-updates` so it picks up GitHub’s advisory database; `open-pull-requests-limit: 10` so a heavy CVE week doesn’t get throttled. PRs from these channels carry the `security` label for filtering. * [x] **G4** — ✅ commit (2026-05-26): `pnpm verify` brought to CI parity. Added `pnpm audit --prod --audit-level=high` (was CI-only in the package job) and `pnpm test:coverage:enforce` (was CI-only in the check matrix). Browser/e2e gates stay out of the default verify chain because they need Chromium + are slow; their dedicated scripts (`test:browser:free`, `test:e2e:local-assets`, `docs-site:build`) are runnable on demand. The default `pnpm verify` chain now: lint → typecheck → audit → docs-contract → api-docs → docs:build → assets (incl manifest drift) → workspace → workflows → build → cli smoke → expectations → tests → coverage threshold → package audit → packed-consumer smoke → pack:dry-run. Mirrors what CI catches except for the browser-Chromium steps. * [x] **G5** — ✅ commit (2026-05-26): `pnpm verify` runs clean end-to-end on `codex/1.0-stabilization-phase-2`. Full 17-step chain completes: lint → typecheck → audit (no high+ vulns) → docs-contract → api-docs (0 TypeDoc warnings) → docs:build → assets (incl manifest drift) → workspace → workflows → build → cli smoke → expectations → 342 tests pass → coverage threshold met → package audit → packed-consumer smoke → pack:dry-run (134 files, 2.3 MB tarball). `audit-package.ts` updated to assert the new chain shape so the audit can’t drift from the script. * [x] **G6** — ✅ commit (2026-05-26): rather than hand-editing `package.json#version` (release-please owns the version field), set `release-as: "1.0.0"` in `release-please-config.json`’s package config. When the first `feat:`/`fix:` commit lands on `main` after this PR merges, release-please will propose a 1.0.0 release PR. The manifest version (`.release-please-manifest.json`) stays at `0.1.0` — release-please bumps it on its own when the release PR merges. This is the canonical release-please flow for promoting pre-1.0 → 1.0. * [ ] \[WAIT] **G7** — PR #4 (`codex/1.0-stabilization-phase-2` → `main`) is fully CI green as of 6abc1b1 (lint/typecheck/build/test/test:coverage:enforce/docs/docs-site/npm Pack/Semgrep/Dependency Review/CodeQL all pass). Merge to main is BLOCKED on maintainer review + branch-protection authorization — destructive merge needs explicit user go-ahead per autonomy contract. * [ ] \[WAIT] **G8** — Post-merge: release-please runs on `main`, sees `release-as: "1.0.0"` (PRD G6), opens a release PR with the 1.0.0 changelog. Maintainer merges the release PR → release-please tags `v1.0.0` (no `v` prefix per config) → `release.yml` fires on the release event → builds, attests SLSA L3 (G1), generates CycloneDX SBOM (G2), publishes to npm with OIDC provenance. Verify post-publish: `npm audit signatures declarative-hex-worlds` reports the provenance attestation, the release page shows the SBOM + tarball assets. ## Self-assessment after each commit [Section titled “Self-assessment after each commit”](#self-assessment-after-each-commit) Before flipping `[ ]` → `[x]`: 1. What did I just ship? Did the visual / behavior match the spec doc? 2. What did the just-finished work surface about the next item? Encode in directive notes if non-trivial. 3. Did this commit introduce any banned pattern? (run gates locally — `pnpm lint && pnpm typecheck`) 4. Did I update relevant docs in the same commit? Drift is a bug. ## Notes [Section titled “Notes”](#notes) * This directive is the authoritative work queue. PRD in `docs/PRD/1.0.md` explains the *why*. * Reviewer trio dispatched per commit: `comprehensive-review:full-review` (background), `security-scanning:security-sast` (background), `code-simplifier` (background). * Visuals: any commit touching `react.ts` / `three.ts` / `examples/` requires a screenshot via vitest-browser before commit. # Post-1.0 maintenance directive (archived) > Archived continuous-work directive for the post-1.0 maintenance phases — coverage closure to 100/100/100/100 (E0), merged-gate wiring, the library-fit decomposition batch (Epic LF), visual-integration gate (E9), docs-site continuation (F-Site), and the comprehensive-review action queue (CR). All items reached [x]/DONE before the RFC-0001 re-architecture began. > **Archived 2026-07-06.** Snapshot of the completed post-1.0 phases from `.agent-state/directive.md`, migrated out of the live directive so the queue tracks only in-flight work. Every item below is `[x]`/DONE. The WHY-per-commit record is preserved here; the live directive now carries only the RFC-0001 milestone. See also [1.0 stabilization](./1.0-stabilization/). *** ## Active queue — post-merge release + maintenance [Section titled “Active queue — post-merge release + maintenance”](#active-queue--post-merge-release--maintenance) ### Phase LF — library-fit decomposition (Epic LF, in PR) [Section titled “Phase LF — library-fit decomposition (Epic LF, in PR)”](#phase-lf--library-fit-decomposition-epic-lf-in-pr) * [x] **LF-PR53** — ✅ PR #53 (Epic LF, branch `feat/library-fit-decomposition`) merged 2026-05-28 as main commit `4f442ba`. All 21 commits across LF1–LF8 squashed in; Gemini review threads addressed in commit `2e58f64` and resolved via `addPullRequestReviewThreadReply` + `resolveReviewThread` graphql mutations; CodeRabbit hit its rate limit so no human-style review threads to reply to; CodeQL `js/incomplete-sanitization` regex meta-escape fix shipped in `7cab6b9`. ### Phase G — release (in-flight) [Section titled “Phase G — release (in-flight)”](#phase-g--release-in-flight) * [x] **G8** — ✅ Release flow exercised end-to-end 2026-05-28, but the deliverable identity changed mid-flight: the original `medieval-hexagon-gameboard@1.0.0` release-please PR (#6) merged + cut a GH release but release.yml failed at workflow setup on a bad `actions/attest-build-provenance` SHA pin (the pinned SHA `e7af5b09…` didn’t exist in the action’s repo — `977bb373…` is the real v3.0.0 SHA). That stale release + tag were deleted; the package was renamed to `declarative-hex-worlds` (PR #55 / commit `b0964bc`), release-please then cut a fresh `declarative-hex-worlds@1.0.0` release via PR #56 (commit `0564ff2`). The npm publish happens locally via `NPMJS_TOKEN` (in `.env`, gitignored); release.yml is then converted to OIDC trusted publishing so subsequent releases need no token. See \[\[rename-to-declarative-hex-worlds]]. ### Phase RB — bootstrap CI integration (continuation) [Section titled “Phase RB — bootstrap CI integration (continuation)”](#phase-rb--bootstrap-ci-integration-continuation) * [x] **RB-CI** — ✅ commit (2026-05-27) established the browser-free visual gate. 2026-06-25 closeout keeps `pnpm test:browser:free` as the full local visual/screenshot gate and wires required CI Coverage through `pnpm coverage:all:enforce` (unit + browser-free coverage merged before threshold enforcement). ### Phase E0 — coverage closure (continuation toward 100/100/100/100) [Section titled “Phase E0 — coverage closure (continuation toward 100/100/100/100)”](#phase-e0--coverage-closure-continuation-toward-100100100100) The A8 coverage ratchet floors at 64.5 / 62.3 / 76.4 / 64 (unit harness) as of `33d271b`. The final PR #210 ratchets merged coverage to 100 / 100 / 100 / 100. * [x] \[DONE] **E0a** — Simulation + patrol files toward 100%. PR#10-#51 merged; no open PR remains for the old PR#52 note as of 2026-06-25. Current CI-measured merged coverage gate after browser-free integration is 76.74 / 70.87 / 84.69 / 76.48 (PR #106 Coverage); local reference-enabled runs read 78.19 / 72.18 / 86.11 / 77.94. CLI command batch PR #108 Coverage measured 78.32 / 72.80 / 85.69 / 78.06 and staged thresholds at 77.8 / 72.3 / 85.1 / 77.5. CLI shared-helper batch PR #109 Coverage measured 79.02 / 73.84 / 86.49 / 78.76 and staged thresholds at 78.3 / 73.0 / 85.6 / 78.0. CLI validation-command batch PR #110 Coverage measured 79.56 / 74.44 / 86.65 / 79.32 and staged thresholds at 78.8 / 73.5 / 85.9 / 78.5. CLI simulation-command batch PR #111 Coverage measured 80.29 / 75.00 / 86.91 / 80.04 and staged thresholds at 79.6 / 74.2 / 86.2 / 79.2. CLI layout/piece-command batch PR #112 Coverage measured 81.04 / 75.76 / 87.14 / 80.81 and staged thresholds at 80.3 / 75.1 / 86.6 / 80.2. CLI piece-registry batch PR #113 Coverage measured 82.30 / 77.02 / 88.60 / 82.08 and staged thresholds at 81.3 / 76.0 / 87.6 / 81.1. CLI summary-variant batch PR #114 Coverage measured 82.54 / 77.44 / 88.60 / 82.33 and staged thresholds at 81.8 / 76.5 / 87.9 / 81.6. CLI guide-output batch PR #115 Coverage measured 82.96 / 77.80 / 88.64 / 82.77 and staged thresholds at 82.2 / 77.0 / 88.0 / 82.0. CLI guide-render batch PR #116 Coverage measured 83.26 / 78.06 / 88.72 / 83.08 and staged thresholds at 82.5 / 77.4 / 88.1 / 82.4. CLI scenario/patrol batch PR #117 Coverage measured 83.98 / 78.47 / 89.18 / 83.82 and staged thresholds at 83.4 / 77.9 / 88.6 / 83.3. CLI declaration/piece batch PR #118 Coverage measured 84.40 / 78.82 / 89.29 / 84.24 and staged thresholds at 83.8 / 78.2 / 88.7 / 83.6. CLI readable command batch PR #119 Coverage measured 85.65 / 79.52 / 89.83 / 85.52 and staged thresholds at 84.8 / 79.1 / 89.4 / 84.8. CLI snapshot/piece batch PR #120 Coverage measured 86.17 / 80.32 / 90.02 / 86.07 and staged thresholds at 85.3 / 79.8 / 89.7 / 85.3. CLI reference generator script batch PR #121 Coverage measured 86.23 / 80.27 / 90.04 / 86.13 and staged thresholds at 85.7 / 79.8 / 89.7 / 85.7. Package-assets generator script batch PR #122 Coverage measured 86.48 / 80.44 / 90.09 / 86.38 and staged thresholds at 86.0 / 79.9 / 89.7 / 85.9. Packed consumer smoke orchestrator batch PR #123 Coverage measured 86.77 / 80.43 / 90.33 / 86.66 and staged thresholds at 86.2 / 80.0 / 90.0 / 86.2. Packed consumer types attestation batch PR #124 Coverage measured 86.81 / 80.41 / 90.38 / 86.70 and staged thresholds at 86.3 / 80.1 / 90.1 / 86.3. Packed consumer install smoke batch PR #125 Coverage measured 87.28 / 80.83 / 90.47 / 87.19 and staged thresholds at 86.7 / 80.5 / 90.2 / 86.7. Simulation engine defensive branch batch PR #126 Coverage measured 87.32 / 80.88 / 90.47 / 87.23 and stages thresholds at 86.8 / 80.6 / 90.3 / 86.8. Selectors edge-mask branch cleanup PR #127 Coverage measured 87.32 / 80.89 / 90.47 / 87.23 and stages thresholds at 86.8 / 80.7 / 90.3 / 86.8. Command handler branch batch PR #128 Coverage measured 87.37 / 80.98 / 90.47 / 87.28 and stages thresholds at 86.9 / 80.8 / 90.3 / 86.9. Ratchet against CI-measured merged coverage after each ≤200 LOC test batch; E8’s 100/100/100/100 flip is complete. NOTE: scenario.ts:1189 (spawn-group warning map) + 1204 (patrol warning map) effectively unreachable — spawn groups never emit warnings; patrol warning in patrol-route planning needs >1 waypoint + 0 segments (contradictory). Skip these arms. * World-rules system branch batch PR #129 Coverage measured 87.37 / 81.01 / 90.47 / 87.28, reached 100 / 100 / 100 / 100 for `src/systems/world-rules-system.ts`, and stages thresholds at 86.9 / 80.9 / 90.3 / 86.9. * Command-dispatch branch cleanup PR #130 Coverage measured 87.37 / 81.02 / 90.47 / 87.28, reached 100 / 100 / 100 / 100 for `src/systems/command-dispatch.ts`, and stages thresholds at 86.9 / 81.0 / 90.3 / 86.9. * Systems/runtime branch cleanup PR #131 Coverage measured 87.44 / 81.27 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/systems/systems.ts`, `src/systems/events.ts`, `src/systems/tick.ts`, `src/runtime/asset-root.ts`, and `src/runtime/runtime.ts`, and staged thresholds at 87.0 / 81.1 / 90.3 / 87.0. * Simulation engine branch closure PR #132 Coverage measured 87.44 / 81.60 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/simulation/engine.ts`, and staged thresholds at 87.0 / 81.2 / 90.3 / 87.0. * Script-validator branch cleanup PR #133 Coverage measured 87.44 / 81.73 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/simulation/script-validators.ts`, and staged thresholds at 87.0 / 81.3 / 90.3 / 87.0. * Pieces branch cleanup PR #134 Coverage measured 87.44 / 81.80 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/pieces/pieces.ts`, and staged thresholds at 87.0 / 81.4 / 90.3 / 87.0. * Occupancy branch cleanup PR #135 Coverage measured 87.44 / 81.81 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/gameboard/occupancy.ts`, and staged thresholds at 87.0 / 81.5 / 90.3 / 87.0. * Manifest schema branch cleanup PR #136 Coverage measured 87.44 / 81.86 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/manifest/schema.ts`, and staged thresholds at 87.0 / 81.6 / 90.3 / 87.0. * Catalog treatments branch cleanup PR #137 Coverage measured 87.44 / 81.89 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/scenario/catalog-treatments.ts`, and staged thresholds at 87.0 / 81.7 / 90.3 / 87.0. * Catalog guide-data branch cleanup PR #138 Coverage measured 87.44 / 81.90 / 90.47 / 87.36, reached 100 / 100 / 100 / 100 for `src/scenario/catalog-guide-data.ts`, and staged thresholds at 87.0 / 81.8 / 90.3 / 87.0. * CLI/source-root branch cleanup PR #139 Coverage measured 88.25 / 82.55 / 91.27 / 88.19, reached 100 / 100 / 100 / 100 for `scripts/smoke/types.ts`, `src/cli/commands/bootstrap/target.ts`, `src/cli/commands/manifest.ts`, `src/cli/commands/validate.ts`, and `src/guides/simple-rpg/exercises.ts`; also closed branch gaps in `src/cli/commands/extract.ts`. Staged thresholds at 87.0 / 81.9 / 90.3 / 87.0. * Interop compatibility branch cleanup PR #140 Coverage measured 88.26 / 82.60 / 91.27 / 88.20, reached 100 / 100 / 100 / 100 for `src/interop/compatibility.ts` after KayKit tile placement, facing defaults, spawn fallback metadata, rigged default roles, idle/first animation fallback, and removal of an unreachable rigged-non-unit warning. Staged thresholds at 87.1 / 82.0 / 90.3 / 87.1. * CLI usage branch cleanup PR #141 Coverage measured 88.29 / 82.60 / 91.31 / 88.23, reached 100 / 100 / 100 / 100 for `src/cli/usage.ts` through direct import coverage of `HELP_TEXT` and `usage()`. Staged thresholds at 87.2 / 82.1 / 90.4 / 87.2. * Bootstrap upstream-layout cleanup PR #142 Coverage measured 88.31 / 82.62 / 91.31 / 88.25, reached 100 / 100 / 100 / 100 for `src/cli/commands/bootstrap/upstream-layout.ts` by covering the FREE-with-units rejection and removing redundant EXTRA units/nullish texture branches. Staged thresholds at 88.1 / 82.4 / 90.9 / 88.1. * CLI validate-plan exit cleanup PR #143 Coverage measured 88.37 / 82.64 / 91.42 / 88.29, reached focused 100 / 100 / 100 / 100 for `src/cli/commands/validate-plan.ts` by exercising the validation-error `process.exit(1)` path; merged report still carries a browser-map statement-id artifact for the same line. Staged thresholds at 88.2 / 82.5 / 91.0 / 88.2. * Gameboard terrain branch cleanup PR #144 Coverage measured 88.38 / 82.70 / 91.42 / 88.30, reached 100 / 100 / 100 / 100 for `src/gameboard/terrain.ts` by covering plan default fallbacks, same-order placement sort, custom tile tags, river crossing overlays, single-edge river masks, and sloped road overlays while removing unreachable internal terrain branches. Staged thresholds at 88.3 / 82.6 / 91.1 / 88.2. * CLI compatibility exit cleanup PR #145 Coverage measured 88.43 / 82.78 / 91.42 / 88.35, reached 100 / 100 / 100 / 100 for `src/cli/commands/compatibility.ts` by covering `--failOnWarning` warning exits and fatal bounds-error exits. Staged thresholds at 88.3 / 82.7 / 91.1 / 88.3. * CLI guide API/role cleanup PR #146 Coverage measured 88.46 / 82.84 / 91.42 / 88.39, reached 100 / 100 / 100 / 100 for `src/cli/commands/guide-apis.ts` and `src/cli/commands/guide-roles.ts` by covering empty selections, unfiltered API summary overflow, and removing unreachable selected-value/nullish and role truncation branches. Staged thresholds at 88.4 / 82.8 / 91.1 / 88.3. * CLI validate-recipe exit cleanup PR #147 Coverage measured 88.48 / 82.87 / 91.46 / 88.41, reached 100 / 100 / 100 / 100 for `src/cli/commands/validate-recipe.ts` by covering readable recipe validation errors and the fatal `process.exit(1)` branch. Staged thresholds at 88.4 / 82.8 / 91.2 / 88.4. * Coordinates projection cleanup PR #148 Coverage measured 88.50 / 82.93 / 91.46 / 88.43, reached 100 / 100 / 100 / 100 for `src/coordinates/projection.ts` by covering sloped road projection, river crossing projection, custom placement tie sorting, and removing unreachable projection defaults/helpers. Staged thresholds at 88.4 / 82.9 / 91.2 / 88.4. * Packed install smoke branch cleanup PR #149 Coverage measured 88.51 / 82.99 / 91.46 / 88.44, reached 100 / 100 / 100 / 100 for `scripts/smoke/pack-install.ts` by covering invalid pack metadata, missing plan/scenario count fallbacks, non-array SimpleRPG exercise evidence, and retained temp-app logging. Staged thresholds at 88.45 / 82.93 / 91.3 / 88.41. * CLI reference generator branch cleanup PR #150 Coverage measured 88.51 / 83.07 / 91.46 / 88.44, reached 100 / 100 / 100 / 100 for `scripts/generate-cli-reference.ts` by covering default generator option resolution, injected IO execution, direct-run predicate branches, and excluding only the thin executable entrypoint guard. Staged thresholds at 88.5 / 83.0 / 91.35 / 88.43. * SimpleRPG executable smoke branch cleanup PR #151 Coverage measured 88.55 / 83.11 / 91.54 / 88.47, reached 100 / 100 / 100 / 100 for `src/guides/simple-rpg/smoke.ts` by covering plan tile fallback/empty-plan failure, recipe error-count helpers, layout fill rule id fallback, and the FREE manifest asset lookup without the literal-key lint warning. Staged thresholds at 88.52 / 83.05 / 91.44 / 88.44. * Package-assets generator branch cleanup PR #152 Coverage measured 88.57 / 83.19 / 91.54 / 88.50, reached 100 / 100 / 100 / 100 for `scripts/generate-package-assets.ts` by covering default/injected dependency resolution, FREE/EXTRA write behavior, source-count failure, argument parsing, and the direct-run predicate while ignoring only the thin executable entrypoint guard. Staged thresholds at 88.55 / 83.14 / 91.5 / 88.46. * CLI snapshot branch cleanup PR #153 Coverage measured 88.61 / 83.21 / 91.58 / 88.53, reached 100 / 100 / 100 / 100 for `src/cli/commands/snapshot.ts` by covering invalid-plan validation exits, recipe compile failure after `allowInvalid`, stdout snapshot output, and plan/recipe/scenario option variants. Staged thresholds at 88.57 / 83.19 / 91.53 / 88.49. * CLI doctor/validate-manifest + scenario registry branch cleanup PR #154 Coverage measured 88.72 / 83.40 / 91.62 / 88.63, reached focused zero-miss coverage for `src/cli/commands/doctor.ts`, `src/cli/commands/validate-manifest.ts`, and `src/scenario/registry.ts` by covering coverage delegation, existing-source/missing-montage reporting, readable empty-manifest summaries, JSON output, fatal manifest validation issue exits, z-origin geometry warnings, terrainless manifest tile inference, default declaration asset/source normalization, object declaration application, custom placement kind defaults, and support-tile registry analysis while removing private unreachable layer/median guards. Staged thresholds at 88.64 / 83.28 / 91.56 / 88.54. * CLI guide-permutations/patrol-script branch cleanup PR #155 Coverage measured 88.83 / 83.50 / 91.70 / 88.74, reached focused zero-miss coverage for `src/cli/commands/guide-permutations.ts` and `src/cli/commands/patrol-script.ts` by covering manifest-backed missing permutation assets, readable missing-asset reporting, assignment-file array variants, missing-assignment validation, readable warning/error reporting, and fatal error/warning exits. Staged thresholds at 88.72 / 83.42 / 91.64 / 88.65. * Coordinates/quests branch cleanup PR #156 Coverage measured 88.89 / 83.67 / 91.70 / 88.78, reached 100 / 98.91 / 100 / 100 for `src/quests/quests.ts` and closed the remaining merged misses for `src/coordinates/coordinates.ts` by covering quest metadata/lookups, reach-tile pending progress, fallback progress preservation, source-less collision evaluation, non-quest entity rejection, canonical-key path reconstruction, spawn passability, and exported heap boundary cases while removing private unreachable coordinate/quest guards. Staged thresholds at 88.78 / 83.55 / 91.64 / 88.70. * CLI command-gap cleanup PR #157 Coverage measured 88.95 / 83.74 / 91.73 / 88.83, advanced `src/cli/commands` to 85.41 / 71.20 / 86.04 / 85.85 and `guide-usages.ts` to 100 / 92.30 / 100 / 100 by covering readable guide summaries, edition-scope and small-result filters, missing manifest asset exits, synthetic source-root extract/declarations success, doctor source reporting, and validation/compatibility fatal exits. Staged thresholds at 88.84 / 83.62 / 91.66 / 88.76. * CLI summarize-scenario cleanup PR #158 Coverage measured 88.98 / 83.88 / 91.73 / 88.86, reached 100 / 93.10 / 100 / 100 for `src/cli/commands/summarize-scenario.ts` by covering validation-error exits, allow-invalid readable summaries without a compiled board, warning-only `failOnWarning` exits, and readable title/top-asset/patrol summary fallbacks. Staged thresholds at 88.87 / 83.76 / 91.67 / 88.79. * Spawn-group/quest branch cleanup PR #159 Coverage measured 89.02 / 83.97 / 91.80 / 88.89, reached focused zero-miss coverage for `src/gameboard/spawn-groups.ts` by covering terrain/elevation/tag candidate filters, found/lower-cost/shorter/empty route selection, and removing unreachable route tie-key and warning-aggregate branches; removed the redundant quest metadata fallback after normalization and kept no-metadata quest spawn coverage. Staged thresholds at 88.90 / 83.86 / 91.74 / 88.82. * Command-handler branch cleanup PR #160 Coverage measured 89.02 / 84.11 / 91.80 / 88.89, reached focused zero-miss coverage for `src/commands/commands.ts` by covering source-less/unreachable/manual move previews, completed same-tile move execution, no-effect handler normalization, empty actor-target selection, single-handler presets, stale actor/placement removals, and successful placement removal while removing post-preview unreachable movement/handler defensive branches. Staged thresholds at 88.91 / 84.00 / 91.75 / 88.84. * Coverage merge location-key cleanup PR #161 Coverage measured 89.50 / 84.06 / 93.03 / 89.42, fixes cross-harness Istanbul statement/function/branch id and column drift by merging counters by source location with unique line fallback and ignoring zero-only unmatched browser entries. Staged thresholds at 89.10 / 84.00 / 92.00 / 89.00. * Dead built-CLI smoke cleanup PR #162 Coverage measured 91.81 / 85.47 / 94.01 / 91.81, removes stale unreferenced `scripts/smoke-built-cli.ts` after `test:cli` was deleted and rewrites current docs/commentary to the actual Vitest/coverage workflow. Staged thresholds at 91.50 / 85.30 / 93.80 / 91.50. * CLI entrypoint dispatcher PR #163 Coverage measured 92.55 / 85.85 / 95.60 / 92.57, reached 100 / 100 / 100 / 100 for `src/cli/cli.ts` through controlled side-effect imports with mocked `citty`, usage, and subcommand modules. Staged thresholds at 92.30 / 85.80 / 95.20 / 92.30. * Showcase promotion script PR #164 Coverage measured 93.07 / 85.96 / 95.88 / 93.07, reached 100 / 74 / 100 / 100 for `scripts/promote-showcases.ts` by extracting injected `promoteShowcases()`/`runPromoteShowcases()` helpers and covering check/promote/failure/quality paths. Staged thresholds at 92.80 / 85.85 / 95.65 / 92.80. * KayKit guide extractor PR #165 Coverage measured 93.57 / 86.22 / 96.32 / 93.60, reached 100 / 78.66 / 100 / 100 for `scripts/extract-kaykit-guide.ts` by extracting injected parser/runner/renderer helpers and covering Swift, portable, Windows lookup, missing-PDF, no-renderer, no-page, and command-failure paths. Staged thresholds at 93.30 / 86.10 / 96.00 / 93.30. * Spawn-groups command PR #166 Coverage measured 93.65 / 86.35 / 96.36 / 93.68 in CI (local reference-enabled proof read 94.01 / 86.68 / 96.55 / 94.03), reached 100 / 100 / 100 / 100 for `src/cli/commands/spawn-groups.ts` by covering missing `--groups`, validation-error exits, JSON output, spawn-plan error exits, and documenting the currently unreachable aggregate-warning exit. Staged thresholds at 93.50 / 86.30 / 96.30 / 93.50. * Blueprint command PR #167 local reference-enabled proof measured 94.33 / 87.35 / 96.74 / 94.34, advanced `src/cli/commands/blueprint.ts` to 98.33 / 93.47 / 90.90 / 99.14 by covering wrapped config parsing, CLI overrides, scenario/interop predicates, payload summaries, readable warnings, JSON output, warning exits, and scenario error exits; defensive output guards stay annotated because `shouldInspectBlueprintScenario` and `shouldEmitBlueprintInterop` make them unreachable through the public command flow. Staged thresholds at 93.90 / 86.90 / 96.40 / 93.90. * Guide-scenarios command PR #168 local reference-enabled proof measured 94.58 / 87.67 / 96.98 / 94.58, reached 100 / 93.10 / 100 / 100 for `src/cli/commands/guide-scenarios.ts` by covering guide filters, include-treatment payloads, missing-manifest asset reporting and exits, markdown stdout, JSON file output, invalid asset-scope errors, and the missing-treatment guard with restored fixture state. Staged thresholds at 94.20 / 87.20 / 96.60 / 94.20. * CLI shared-helper PR #169 local reference-enabled proof measured 95.06 / 88.12 / 97.17 / 95.08, advanced `src/cli/_shared.ts` to 87.86 / 83.16 / 94.12 by covering registry and piece-registry validation, output emission, GLTF metadata extraction, piece flag/source URL parsing, and readable helper reports. Staged thresholds at 94.60 / 87.70 / 96.80 / 94.60. * Patrol-routes command PR #170 local reference-enabled proof measured 95.15 / 88.27 / 97.21 / 95.17, advanced `src/cli/commands/patrol-routes.ts` to 95.83 / 90.90 / 100 / 95.74 by covering missing routes, malformed scenarios, validation exits, spawn-group exits, JSON and file output, scenario seed fallbacks, and route-error readable output. The remaining route warning/fail-on-warning arm depends on the documented contradictory route-planning warning path. Staged thresholds at 94.70 / 87.80 / 96.85 / 94.70. * Guide-assets command PR #171 local reference-enabled proof measured 95.25 / 88.45 / 97.36 / 95.25, reached 100 / 97.67 / 100 / 100 for `src/cli/commands/guide-assets.ts` by covering single-asset selected payloads, file output, readable small/overflow summaries, each empty-selection filter branch, and the no-match command error. Staged thresholds at 94.80 / 88.00 / 97.00 / 94.80. * Place-piece command PR #172 local reference-enabled proof measured 95.37 / 88.66 / 97.40 / 95.38, reached 100 / 97.29 / 100 / 100 for `src/cli/commands/place-piece.ts` by covering input validation, piece-registry selection failures, JSON/file/readable output, out-plan writes, invalid-plan exits, no-placement min-count exits, and seed/id-prefix placement output. The remaining coordinate-string readable branch is defensive because generated piece placements use coordinate objects. Staged thresholds at 94.90 / 88.10 / 97.10 / 94.90. * Analyze-layout command PR #173 local reference-enabled proof measured 95.48 / 88.88 / 97.44 / 95.49, reached 100 / 100 / 100 / 100 for `src/cli/commands/analyze-layout.ts` by covering input/rules validation, rule array and object parsing, file and CLI seed resolution, JSON/file/readable output, out-plan writes, invalid-plan exits, error exits, and fail-on-warning exits. Staged thresholds at 95.00 / 88.30 / 97.20 / 95.00. * Summarize-plan command PR #174 local reference-enabled proof measured 95.66 / 89.09 / 97.52 / 95.67, reached 100 / 100 / 100 / 100 for `src/cli/commands/summarize-plan.ts` by covering exact-input and JSON-shape validation, recipe/scenario compile exits, plan validation exits, validation warning exits, readable none/EXTRA top-asset branches, out/outPlan writes, and blueprint/config input aliases. Staged thresholds at 95.20 / 88.60 / 97.30 / 95.20. * Simulate-scenario command PR #175 local reference-enabled proof measured 95.82 / 89.41 / 97.59 / 95.83, reached 100 / 92.42 / 100 / 100 for `src/cli/commands/simulate-scenario.ts` by covering missing-input and malformed-scenario guards, scenario/script validation exits, JSON success and expectation-failure outputs, readable expectation-failure summaries, blocked-quest exits, and actor-target/mutation readable fallbacks. Staged thresholds at 95.30 / 88.90 / 97.40 / 95.30. * Piece command PR #176 local reference-enabled proof measured 95.85 / 89.54 / 97.59 / 95.86, reached 100 / 100 / 100 / 100 for `src/cli/commands/piece.ts` by covering missing-asset validation, derived asset and piece ids, default source and omitted role/report branches, explicit attribution/file output, warning exits, and fatal report error exits. Staged thresholds at 95.40 / 89.00 / 97.40 / 95.40. * Pieces command PR #177 local reference-enabled proof measured 95.91 / 89.68 / 97.59 / 95.92, reached 100 / 100 / 100 / 100 for `src/cli/commands/pieces.ts` by covering missing-input and conflicting placement-input validation, rule/source-url/placement payload variants, file output, registry analysis errors, registry warning exits, and placement-warning exits while marking the defensive placement-error guard unreachable because registry analysis blocks invalid selections before inspection. Staged thresholds at 95.45 / 89.10 / 97.40 / 95.45. * Bootstrap core PR #178 local reference-enabled proof measured 96.32 / 90.15 / 97.59 / 96.36, reached 100 / 100 / 100 / 100 for `src/cli/commands/bootstrap/core.ts` by covering default output resolution, same-size hash drift, non-empty targets without sidecars, malformed/oversized/escaping sidecars, source-format inclusion under the GLTF root, missing texture directories, deep/corrupt zip failures, successful mocked GitHub downloads, stream cleanup, and HTTP failure fallbacks; fixed extension parsing to use the file basename and marked only defensive library/filesystem race guards unreachable through the public zip path. Staged thresholds at 95.80 / 89.70 / 97.40 / 95.80. * Validate-simulation command PR #179 local reference-enabled proof measured 96.36 / 90.25 / 97.63 / 96.39, reached 100 / 100 / 100 / 100 for `src/cli/commands/validate-simulation.ts` by covering missing scenario/script guards, non-object scenario validation, JSON/readable output fallbacks with empty actor/quest lists, actor/quest JSON mapping, out-plan writes, and fatal script validation exits while simplifying redundant script-step array guards after `readSimulationScript` validation. Staged thresholds at 95.90 / 89.80 / 97.40 / 95.90. * Validate-scenario command PR #180 local reference-enabled proof measured 96.39 / 90.41 / 97.71 / 96.41, reached 100 / 100 / 100 / 100 for `src/cli/commands/validate-scenario.ts` by covering the missing-scenario guard, compiled-plan output, JSON/runtime actor and quest payloads, authored/empty fallback payloads when validation errors skip runtime creation, readable spawn/patrol summaries, and fatal validation exits while simplifying the unreachable runtime-plan fallback after inspection already compiles the plan. Staged thresholds at 96.00 / 89.90 / 97.40 / 96.00. * Pieces-from-assets command PR #181 local reference-enabled proof measured 96.57 / 90.74 / 97.75 / 96.60, reached 100 / 100 / 100 / 100 for `src/cli/commands/pieces-from-assets.ts` by covering missing inputs, no-model scans, override payload validation, single-file and multi-root asset inputs, absolute-path/includeReport payloads, prefix/tag file output, override-warning fail-on-warning exits, registry warning/error printing, and fatal registry-error exits while pairing scanned paths with source records and relying on the scanned-root invariant for relative asset paths. Staged thresholds at 96.10 / 90.20 / 97.40 / 96.10. * Guide-render-requests command PR #182 local reference-enabled proof measured 96.64 / 90.84 / 97.79 / 96.66, reached 100 / 100 / 100 / 100 for `src/cli/commands/guide-render-requests.ts` by covering empty selections, readable URL output, JSON grouped payloads, edition-scoped file output, missing manifest asset reporting, fatal missing-asset exits, and request overflow summaries. Staged thresholds at 96.20 / 90.30 / 97.40 / 96.20. * Guide-assets command PR #183 local reference-enabled proof measured 96.64 / 90.85 / 97.79 / 96.66, reached 100 / 100 / 100 / 100 for `src/cli/commands/guide-assets.ts` by removing the unreachable selected-asset nullish fallback after the `assetIdFilter.length === 1` guard. Staged thresholds at 96.20 / 90.40 / 97.40 / 96.20. * Summarize-scenario command PR #184 local reference-enabled proof measured 96.64 / 90.88 / 97.79 / 96.66, reached 100 / 100 / 100 / 100 for `src/cli/commands/summarize-scenario.ts` by covering readable EXTRA actor asset output, including the top-asset `*` suffix and non-empty actor extra asset list. Staged thresholds at 96.20 / 90.50 / 97.40 / 96.20. * CLI command-output PR #185 local reference-enabled proof measured 96.68 / 90.98 / 97.87 / 96.69, reached 100 / 100 / 100 / 100 for `src/cli/commands/place-piece.ts` and advanced `blueprint`, `patrol-routes`, and `simulate-scenario` command-output gaps through blueprint default-shape/error/warning/scenario-validation exits, readable actor-target and mutation output, and documented defensive display branches with no current public producer. Staged thresholds at 96.30 / 90.60 / 97.50 / 96.30. * CLI branch-artifact PR #186 local reference-enabled proof measured 96.69 / 91.08 / 97.87 / 96.70, reached 100 / 100 / 100 / 100 for `src/cli/commands/blueprint.ts`, `src/cli/commands/patrol-routes.ts`, and `src/cli/commands/simulate-scenario.ts` by covering the empty patrol-route display, JSON simulation report file output, and reducing redundant CLI blueprint numeric flag branches. Staged thresholds at 96.30 / 90.70 / 97.50 / 96.30. * Gameboard patrol-route PR #187 local reference-enabled proof measured 96.81 / 91.19 / 97.91 / 96.81, reached 100 / 100 / 100 / 100 for `src/gameboard/patrol-routes.ts` by covering explicit-start/default route diagnostics, open non-loop route warnings, route-set warning aggregation, and documenting public-path-unreachable waypoint-pair invariants. Staged thresholds at 96.40 / 90.80 / 97.50 / 96.40. * Gameboard navigation PR #188 local reference-enabled proof measured 96.92 / 91.37 / 97.95 / 96.91, reached 100 / 100 / 100 / 100 for `src/gameboard/navigation.ts` by covering blocked-start/blocked-goal path branches, reachability sorting and wrapper output, movement-cost fallbacks, malformed key diagnostics, and documented heap/map invariants maintained by the exported planners. Staged thresholds at 96.50 / 91.00 / 97.60 / 96.50. * Patrol runtime PR #189 local reference-enabled proof measured 96.97 / 91.57 / 97.99 / 96.96, reached 100 / 100 / 100 / 100 for `src/patrol/patrol.ts` by covering route snapshot sorting, current-waypoint fallbacks, blocked movement fallback context, resetless blocked movement requests, completed-route wrap/end states, missing-trait diagnostics, and sparse waypoint invariant errors while removing redundant post-normalization segment-cost fallback branching. Staged thresholds at 96.60 / 91.20 / 97.70 / 96.60. * Scenario branch cleanup PR #190 CI-measured merged proof measured 96.65 / 91.55 / 97.91 / 96.65, reached 100 / 95.95 / 100 / 100 for `src/scenario/scenario.ts` by covering collision objective tile targets, deep clone coverage for scenario metadata/navigation/agent options, actor asset aggregation, and documenting only public-path-unreachable spawn-resolution and warning diagnostics. Staged thresholds at 96.50 / 91.40 / 97.80 / 96.50. * Movement edge cleanup local merged proof measured 97.14 / 92.00 / 98.10 / 97.14, reached 100 / 98.18 / 100 / 100 for `src/movement/movement.ts` by covering stale completed paths, malformed sparse path diagnostics, newly blocked stale paths, missing board/placement state diagnostics, no-agent movement-budget fallback, and fixing movement-agent writes to add the trait when absent. Staged thresholds at 96.70 / 91.60 / 97.90 / 96.70. * Movement final branch cleanup local merged proof measured 97.14 / 92.02 / 98.10 / 97.14, reached 100 / 100 / 100 / 100 for `src/movement/movement.ts` by covering the idle path-state fallback and removing the unreachable entity-side missing-placement error branch. Staged thresholds at 96.70 / 91.65 / 97.90 / 96.70. * Showcase promotion branch cleanup local merged proof measured 97.14 / 92.18 / 98.10 / 97.14, reached 100 / 100 / 100 / 100 for `scripts/promote-showcases.ts` by covering default workspace/target/dependency fallbacks, promotion-mode runner success output, and `Error` quality analyzer reporting. Staged thresholds at 96.70 / 91.75 / 97.90 / 96.70. * KayKit guide extractor branch cleanup local merged proof measured 97.14 / 92.36 / 98.10 / 97.14, reached 100 / 100 / 100 / 100 for `scripts/extract-kaykit-guide.ts` by covering default dependency/repo-root fallbacks, process-platform command detection, non-`Error` runner failures, null Swift exit status fallback, and simplifying page-number sorting to the already-filtered filename invariant. Staged thresholds at 96.70 / 91.90 / 97.90 / 96.70. * Packed consumer smoke branch cleanup local merged proof measured 97.17 / 92.52 / 98.10 / 97.17, reached 100 / 100 / 100 / 100 for `scripts/smoke-packed-consumer.ts` by covering non-`Error` phase failures, default context/process/filesystem fallbacks, default runner dependency wiring on safe failure paths, and the injectable direct-run predicate while ignoring only the thin executable guard. Staged thresholds at 96.75 / 92.00 / 97.90 / 96.75. * Scenario final branch cleanup local merged proof measured 97.17 / 92.72 / 98.10 / 97.17, reached 100 / 100 / 100 / 100 for `src/scenario/scenario.ts` by covering unknown-id diagnostics, omitted scenario arrays/objectives, sparse navigation clone options, unresolved-summary actor copy variants, and removing redundant claimant/spawn-index invariant fallbacks. Staged thresholds at 96.75 / 92.20 / 97.90 / 96.75. * Scenario catalog branch cleanup local merged proof measured 97.25 / 92.83 / 98.22 / 97.25, reached 100 / 100 / 100 / 100 for `src/scenario/catalog.ts` by covering texture/faction/unit guards, empty edition-scope usage filters, Markdown option branches, missing-treatment usage/role-count behavior, and preserving default asset-id sort order. Staged thresholds at 96.80 / 92.30 / 98.00 / 96.80. * Ingest duplicate branch cleanup local merged proof measured 97.35 / 93.08 / 98.22 / 97.36, reached 100 / 100 / 100 / 100 for `src/ingest/ingest.ts` by covering missing manifest roots, manifest JSON output, duplicate asset-id suffix allocation, sparse GLTF metadata, optional texture discovery, unsupported categories, symlink skipping, and simplifying private fallback branches to the generated-manifest invariants. Staged thresholds at 96.90 / 92.60 / 98.00 / 96.90. * script-validators.ts 100 / 100 / 100 / 100; remaining simulation-surface gaps are in assertions/patrol/report-adjacent helpers, not authored-script validators. * engine.ts 100 / 100 / 100 / 100 — direct mutation defaults, patrol simulation route-set branch, actor-target default spreads, and unreachable koota placement-state fallbacks closed. * assertions.ts 100 / 100 / 100 / 100 — expectation branches and actor-target matching are zero-miss in merged coverage. * report.ts 100 / 100 / 100 / 100 — copyQuestObjective tile/targetTile branches done. * patrol.ts 100 / 100 / 100 / 100 — route sorting, blocked fallback context, completed wrap/end transitions, and malformed entity diagnostics closed. * layout.ts 87.13 → ratcheting; normalizeArchetypes, resolveArchetype, selectLayout count=0. * quests.ts merged zero-miss after metadata normalization cleanup; historical objective rollover coverage remains in place. * manifest/schema.ts 100 / 100 / 100 / 100 — unit-style exclusion plus malformed-id diagnostic fallback branches closed. * scenario/catalog-treatments.ts 100 / 100 / 100 / 100 — source-path sort tie plus fallback neutral-structure and prop treatment branches closed. * scenario/catalog-guide-data.ts 100 / 100 / 100 / 100 — private guide scenario input now requires authored public APIs instead of carrying an unreachable fallback. * scenario/catalog.ts 100 / 100 / 100 / 100 — public taxonomy guards, usage filter edges, Markdown option branches, missing-treatment guards, and asset-id sort behavior closed. * actors.ts 88.93 → register + navigationProfile action wrappers. * pieces.ts 100 / 100 / 100 / 100 — declaration asset defaults, pooled object-archetype checks, placement count fallbacks, and compatibility override tags closed. * scenario/scenario.ts 100 / 100 / 100 / 100 — clone/summary option branches, collision target tile validation, unknown scenario ids, optional quest objectives, allocator claimant labels, unset spawn indexes, omitted cloned navigation fields, and unresolved-summary actor copies closed. * gameboard.ts 92.21 → addUnitPreset role variants. * bootstrap.ts 75 → verifyBootstrap unsafe-sidecar + missing-file. * movement.ts 100 / 100 / 100 / 100 — stale completed paths, idle fallback, sparse path diagnostics, blocked stale paths, missing board/placement state diagnostics, no-agent budget fallback, and add-if-absent agent writes covered. * interop/compatibility.ts 100 / 100 / 100 / 100 — tile-compatible placement, facing defaults, spawn fallback metadata, rigged default roles, animation fallback chain, and unreachable rigged-non-unit warning removed. * cli/usage.ts 100 / 100 / 100 / 100 — direct import covers `HELP_TEXT`, console output, and requested exit code. * cli/commands/bootstrap/upstream-layout.ts 100 / 100 / 100 / 100 — FREE-with-units rejection covered; redundant EXTRA units and nullish texture fallback branches removed. * cli/commands/validate-plan.ts 100 / 100 / 100 / 100 — validation-error output plus `process.exit(1)` branch covered. * cli/commands/compatibility.ts 100 / 100 / 100 / 100 — warning and fatal error `process.exit(1)` branches covered. * cli/commands/guide-apis.ts 100 / 100 / 100 / 100 — empty selection, selected payload, and readable overflow summary covered. * cli/commands/guide-roles.ts 100 / 100 / 100 / 100 — empty selection and selected payload covered; fixed readable output to list all current roles instead of carrying unreachable overflow. * cli/commands/validate-recipe.ts 100 / 100 / 100 / 100 — readable validation errors plus fatal `process.exit(1)` branch covered. * coordinates/projection.ts 100 / 100 / 100 / 100 — sloped road/crossing projection, custom placement tie sort, and unreachable defaults/helpers closed. * gameboard/terrain.ts 100 / 100 / 100 / 100 — default plan fallbacks, placement tie sort, authored tags, river crossing/single-edge river overlays, and sloped road overlays covered; unreachable internal branches removed. * gameboard/spawn-groups.ts, gameboard/patrol-routes.ts, and gameboard/navigation.ts 100 / 100 / 100 / 100. * ingest.ts 100 / 100 / 100 / 100 — duplicate suffix allocation, sparse GLTF metadata, texture discovery, symlink skip, missing-root errors, and manifest JSON output covered; private fallback invariants simplified. * Browser branch-map cleanup batch local merged proof measured 97.35 / 93.16 / 98.22 / 97.36, reached merged zero-miss coverage for `src/coordinates/projection.ts`, `src/gameboard/terrain.ts`, and `src/simulation/engine.ts` by exercising river-crossing projection/terrain and direct simulation mutation system branches in Chromium while simplifying actor-update mutation reads to use the updated entity. Staged thresholds at 96.90 / 92.70 / 98.00 / 96.90. * Gameboard/validation branch cleanup local merged proof measured 97.61 / 93.74 / 98.21 / 97.62, reached merged zero-miss coverage for `src/gameboard/gameboard.ts` and `src/rules/validation.ts` by covering builder default/variant/boundary branches, invalid authored placement/connectivity validation diagnostics, EXTRA asset references, and declaration adjacency boundaries while documenting only current public-path-impossible fallback guards. Staged thresholds at 97.10 / 93.20 / 98.00 / 97.10. * Seeded rules branch cleanup local merged proof measured 97.78 / 94.10 / 98.29 / 97.80, reached merged zero-miss coverage for `src/rules/rules.ts` by covering seeded board defaults/degenerate shapes, density override exclusivity, piece-fill registry errors/empty selections/object archetypes, and option-level layout archetype fills while documenting deterministic built-in shape/path invariants. Staged thresholds at 97.30 / 93.60 / 98.10 / 97.30. * Recipe branch cleanup local merged proof measured 97.84 / 94.60 / 98.40 / 97.86, reached merged zero-miss coverage for `src/scenario/recipe.ts` by covering remaining serializable step dispatch and clone paths, raw malformed action diagnostics, empty/no-seed/piece-only generation, local/global archetype merges, and deep criteria footprint/distance/preference cloning while fixing `setTileAsset` recipe tag cloning. Staged thresholds at 97.40 / 94.10 / 98.20 / 97.40. * Koota branch cleanup local merged proof measured 98.00 / 94.96 / 98.52 / 98.03, reached merged zero-miss coverage for `src/koota/koota.ts` by covering stale placement-index collision recovery, trait-corruption error paths, sparse occupancy relations, equal-order placement/occupancy sorting, default layer dispatch, computed placement offsets, and invalid tile-key inspection while simplifying query-required placement state reads. Staged thresholds at 97.50 / 94.50 / 98.30 / 97.50. * Blueprint branch cleanup local merged proof measured 98.19 / 95.62 / 98.52 / 98.23, reached merged zero-miss coverage for `src/scenario/blueprint.ts` by covering tiny-board diagnostics, authored town/harbor/road/river fallback IDs, generated defaults, semantic prop-cluster dressing, bridge de-duplication, empty-shape ridge failures, and defensive blueprint compiler invariants. Staged thresholds at 97.70 / 95.10 / 98.30 / 97.70. * Three branch cleanup local merged proof measured 98.39 / 95.99 / 98.75 / 98.44, reached merged zero-miss coverage for `src/three/three.ts` by covering asset URL defaults, animation URL overrides and clip fallbacks, transform/framing helpers, recursive user-data tagging, interaction lookup misses, stale-removal branches, no-parent sync, and sync load error collection/rethrow paths. Staged thresholds at 97.90 / 95.50 / 98.50 / 97.90. * Interop branch cleanup local merged proof measured 98.53 / 96.84 / 98.79 / 98.58, reached merged zero-miss coverage for `src/interop/interop.ts` by covering base snapshot inclusion flags, spawn candidates, legacy adjacency relation fallback, missing relation target reporting, out-of-board footprint filtering, scenario inclusion flags, unresolved actor/quest relation skips, spawn/patrol planning errors, runtime snapshot inclusion flags, and simulation report fallback variants while documenting only public-path-impossible interop serialization invariants. Staged thresholds at 98.00 / 96.30 / 98.50 / 98.00. * Layout branch cleanup local merged proof measured 98.79 / 97.74 / 98.87 / 98.85, reached merged zero-miss coverage for `src/coordinates/layout.ts` by covering layout archetype fallback registries, custom/radius/adjacent footprints, harbor facing inference, fill diagnostics, default layers, sparse serialized plans, and fill/spawn default seeds while documenting only public-path-impossible analyzer and slot-angle invariants. Staged thresholds at 98.30 / 97.20 / 98.60 / 98.30. * Actors branch cleanup local merged proof measured 99.15 / 98.79 / 98.99 / 99.23, reached merged zero-miss coverage for `src/actors/actors.ts` by covering actor kind inference, selector/string fallbacks, collision profiles, neighborhood aggregation, interaction command edges, actor-target sorting, disconnected target paths, and documenting only public-path-impossible ECS corruption/route sentinel invariants. Staged thresholds at 98.60 / 98.30 / 98.80 / 98.70. * CLI shared-helper cleanup local merged proof measured 99.65 / 99.57 / 99.18 / 99.75, reached 100 / 100 / 100 / 100 for `src/cli/_shared.ts` by covering manifest/registry validation, synthetic source manifest generation, plan reader exits, patrol route-set fallbacks, GLB metadata chunks, piece selection/source branches, and readable report edge cases while simplifying only guarded route and asset-id fallbacks. Staged thresholds at 99.30 / 99.20 / 99.00 / 99.40. * React cleanup local merged proof measured 100 / 100 / 100 / 100, reached merged zero-miss coverage for `src/react/react.ts` by covering provider factories, action-hook factories, empty-world fallbacks, coordinate-object tile hooks, queued revision disposal, and browser interop spawn-location merge counters while simplifying only unreachable/internal option guards. Remaining gaps: none in merged coverage. The explicit `script.ts`, `script-validators.ts`, bootstrap core, `gameboard/gameboard.ts`, `rules/validation.ts`, `rules/rules.ts`, `scenario/recipe.ts`, `scenario/blueprint.ts`, `koota/koota.ts`, `three/three.ts`, `interop/interop.ts`, `coordinates/layout.ts`, `actors/actors.ts`, `cli/_shared.ts`, and `react/react.ts` gaps are closed. * [x] \[DONE] **E0h** — Sweep remaining src/ files to 100% (paired with E0a per-PR). PR#10 closures advanced many of these in tandem with E0a. Status post-merge: * `pieces/pieces.ts` 100 / 100 / 100 / 100 in focused and merged unit coverage after declaration defaults, pooled-rule compatibility, placement fallback, and override-tag branch closure * `gameboard/occupancy.ts` 100 / 100 / 100 / 100 in merged coverage after explicit layout-footprint opt-out branch closure * `quests/quests.ts` merged zero-miss after metadata normalization cleanup; quest objective rollover + reward dispatch coverage remains in the existing slices * `actors/actors.ts` 100 / 100 / 100 / 100 in focused and merged coverage after actor kind inference, selector/string fallbacks, collision profiles, neighborhood aggregation, interaction command edges, actor-target sorting, disconnected target paths, and defensive ECS/route invariant cleanup. * `scenario/scenario.ts` 100 / 100 / 100 / 100 in merged coverage after unknown-id diagnostics, omitted scenario arrays/objectives, sparse navigation clone options, unresolved-summary actor copy variants, and invariant fallback cleanup * `scenario/blueprint.ts` 100 / 100 / 100 / 100 in merged coverage after tiny-board diagnostics, authored fallback ids/defaults, semantic prop-cluster dressing, bridge de-duplication, and defensive compiler invariants * `scenario/catalog.ts` 100 / 100 / 100 / 100 in merged coverage after public taxonomy guards, usage filter edges, Markdown option branches, missing-treatment guards, and asset-id sort behavior * `ingest/ingest.ts` 100 / 100 / 100 / 100 in merged coverage after duplicate suffix allocation, sparse GLTF metadata, texture discovery, symlink skip, missing-root errors, and manifest JSON output * `manifest/schema.ts` 100 / 100 / 100 / 100 in merged coverage after manifest normalization/filter combinations * `gameboard/navigation.ts` 100 / 100 / 100 / 100 in merged coverage after pathfinding and reachability edge branches * `coordinates/layout.ts` 100 / 100 / 100 / 100 in merged coverage after layout archetype fallback registries, custom/radius/adjacent footprints, harbor facing inference, fill diagnostics, default layers, sparse serialized plans, and fill/spawn default seed branch coverage. * `three/three.ts` 100 / 100 / 100 / 100 in merged coverage after URL fallback, animation fallback, transform/framing helper, user-data lookup, and sync error/stale branch coverage. * `interop/interop.ts` 100 / 100 / 100 / 100 in merged coverage after snapshot option, relation fallback, scenario/runtime/simulation projection, and defensive serialization invariant coverage. * `cli/_shared.ts` 100 / 100 / 100 / 100 in merged coverage after manifest/registry validation, synthetic source manifest generation, plan reader exits, patrol route-set fallbacks, GLB metadata chunks, piece selection/source branches, and readable report edge cases. * `react/react.ts` 100 / 100 / 100 / 100 in merged coverage after provider/action hook factories, empty-world fallbacks, coordinate-object tile helpers, queued revision disposal, and browser interop spawn-location counter closure. * `scenario/recipe.ts` merged zero-miss after serializable step dispatch and clone branch coverage, empty/no-seed/piece-only generation coverage, local/global archetype merge coverage, malformed action diagnostics, and `setTileAsset` tag clone hardening. * `patrol/patrol.ts` 100 / 100 / 100 / 100 in merged coverage after route snapshot sorting, blocked context fallbacks, completed-route wrap/end states, and malformed trait diagnostics. * `src/cli/commands/{blueprint,summarize-plan,snapshot,spawn-groups,patrol-routes}.ts` advanced by a direct-import blueprint-derived command batch; `src/cli/_shared.ts` advanced by guide-filter, authored-option, piece-source-root, and scalar-helper coverage; `validate-plan`, `validate-recipe`, `validate-scenario`, `validate-simulation`, and `summarize-scenario` advanced by docs-fixture happy paths; `simulate-scenario` and `patrol-script` advanced by report/output happy paths; `validate-manifest`, `analyze-layout`, and `place-piece` advanced by docs/blueprint fixture success paths; `doctor` and `validate-manifest` reached focused 100 / 100 / 100 / 100 through coverage delegation, existing-source/missing-montage reporting, readable empty-manifest summaries, JSON output, and fatal manifest validation issue exits; `scenario/registry.ts` reached focused zero-miss coverage through z-origin geometry warnings, terrainless manifest tile inference, default declaration asset/source normalization, object declaration application, custom placement kind defaults, and support-tile registry analysis; `pieces-from-assets` and `pieces` advanced by local GLTF fixture registry/rules/source URL paths; `summarize-plan`, `patrol-routes`, `validate-scenario`, `validate-simulation`, and `summarize-scenario` advanced through recipe/scenario/blueprint input variants and JSON/output paths; `guide-assets`, `guide-roles`, `guide-scenarios`, and `guide-usages` advanced through filtered file output, markdown output, and readable summaries; `guide-apis`, `guide-permutations`, and `guide-render-requests` advanced through filtered file output, grouped render requests, asset-base URLs, and readable summaries; `summarize-scenario`, `patrol-routes`, and `patrol-script` advanced through readable summaries, assignment-file variants, and malformed payload guards; `declarations`, `analyze`, `compatibility`, and `piece` advanced through packaged-manifest output, readable analysis, synthetic GLTF compatibility, and piece declaration output; readable command-output variants advanced `summarize-plan`, `validate-plan`, `validate-recipe`, `validate-scenario`, `validate-simulation`, `spawn-groups`, `analyze-layout`, and `place-piece` through readable report paths; `blueprint`, `snapshot`, `pieces-from-assets`, and `pieces` advanced through readable blueprint/scenario inspection, plan/recipe snapshot variants, GLTF asset scans, registry analysis, rules, source URLs, and piece-fill placement output. `scripts/smoke/pack-install.ts` advanced through dependency-injected packed install, CLI assertion, temp-write, and generated source coverage. The larger CLI command surface still has low-coverage command modules. Each file’s continuation work lands as one commit that adds ≤200 LOC of test code and ratchets the floor. * [x] \[DONE] **E8** — Flip coverage thresholds to **100 / 100 / 100 / 100** in `vitest.coverage.shared.ts`. Final React cleanup local merged Coverage proof is 100 / 100 / 100 / 100 and the remaining `react/react.ts` gap is closed. ### Phase E-MergedGate — wire browser coverage into the enforced gate [Section titled “Phase E-MergedGate — wire browser coverage into the enforced gate”](#phase-e-mergedgate--wire-browser-coverage-into-the-enforced-gate) **User decision (2026-05-28):** the enforced CI gate (`test:coverage:enforce`) is unit-only, but it measures `src/react/react.ts` + `src/three/three.ts` which Node can never cover (292 uncov lines / 134 uncov fns dragging the gate \~1.5–4pt). Chosen fix: **wire browser-free coverage into the gate** (merge unit + browser-free, enforce on merged) so react/three are measured where they CAN be covered — NOT exclude them. Closeout (2026-06-25): 1. **\[DONE]** Stale browser alias/import safety work from LF8 remains green; `tests/unit/umbrella-browser-safe.test.ts` asserts zero `node:*` leaks from the umbrella. 2. **\[DONE]** Vitest Browser hang fixed: `publicDir: packageRoot` was shadowing Vitest Browser’s client; browser-free now uses `publicDir: false`, serves bootstrapped GLTFs through `/@fs//models`, and has an explicit `@vitest/runner` dev dependency plus browser harness smoke coverage. 3. **\[DONE]** Local visual gate is green: `pnpm exec tsx src/cli/cli.ts bootstrap --source github --out models`; `pnpm test:browser:free`. 4. **\[DONE]** Merged coverage gate is enforced: `scripts/merge-coverage.ts` runs `nyc check-coverage` when `HEX_WORLDS_COVERAGE_ENFORCE=1`; `pnpm coverage:all:enforce` passed locally at 78.19 / 72.18 / 86.11 / 77.94 and `.github/workflows/ci.yml` Coverage now bootstraps FREE models, installs Playwright Chromium, and enforces the merged unit+browser-free tree. **Layering decisions (2026-05-28, user-directed during link-3 investigation):** * The 1100-line root `src/index.ts` hand-relisted 956 named symbols that the 18 domain barrels already `export *` — pure duplication, not curation (tiering is enforced at package.json#exports + @internal, not the root hand-list). **Root rewritten to barrel-forwarding** (`export * from './'`). Adding a public export to a domain now needs no root edit. * There is **NO server/browser split and NO `server.ts`** (rejected as a wrapper shim). The umbrella is browser-safe by *correct layering*: it exports library/runtime API only. `bootstrap` is a **CLI-domain capability** (reachable ONLY from `src/cli/`; never from runtime/react/three) — it was wrong to re-export it from the umbrella at all. Bootstrap stays reachable via the `./bootstrap` subpath for programmatic CLI-equivalent callers. * `manifest/upstream-layout.ts` was **mis-filed**: its data + fs-probers are consumed ONLY by bootstrap (CLI-domain), nothing in manifest/runtime uses it. **Move `src/manifest/upstream-layout.ts` → `src/bootstrap/upstream-layout.ts`**; published subpath renamed `./manifest/upstream-layout` → `./bootstrap/upstream-layout` (pre-1.0; update docs-site 3 refs + smoke/types.ts + architecture table + biome restricted-imports + tsup entry + package.json exports). bootstrap barrel re-exports it. **Ordered sequence:** The LF batch below remains the historical decomposition that unblocked browser-safe imports. Step C and Step D are complete as of 2026-06-25; the remaining coverage work is E0a/E0h/E8’s progressive 100% ratchet, not the browser/merge gate itself. ## Batch — library-fit-decomposition (batch-20260528-103351) [Section titled “Batch — library-fit-decomposition (batch-20260528-103351)”](#batch--library-fit-decomposition-batch-20260528-103351) Source: docs/plans/library-fit-decomposition.prq.md (sha256: 42bbb50d5a7041055a2a67438c99192629f0f2264f89871989985fdb892d6f96) Started: 2026-05-28T10:33:51Z ### task-LF1 Barrel-forwarding umbrella + internal extraction (merged LF1+LF1b) [Section titled “task-LF1 Barrel-forwarding umbrella + internal extraction (merged LF1+LF1b)”](#task-lf1-barrel-forwarding-umbrella--internal-extraction-merged-lf1lf1b) * [x] task-LF1 ✅ (commit, 2026-05-28) Rewrite src/index.ts to `export * from './'` only (18 library barrels); drop bootstrap re-exports. Extract the 28 internal helpers the barrels over-export so they’re shareable-but-private: * DISCOVERY (2026-05-28): the old hand-list root was NOT pure duplication — it curated OUT 28 internal helpers the domain barrels over-export via `export *`. Barrel-forwarding surfaces them; they must leave the public boundary. * **Generic cross-cutting → new `src/internal/` (NOT in package.json#exports):** errorMessage, isNonEmptyString, isRecord (dedupe the cli/\_shared.ts copy too), includesString, isHexCoordinatesInput, tryParseHexKey, plus the type-guards/const-arrays that are pure utilities. * **Domain-specific internals stay in their domain file but leave the public boundary** (the barrel/shim re-exports only public names; siblings import directly): report.ts internals (actorRecord/…/simulationResult), script.ts SIMULATION\_\* arrays + isSimulation\* guards + tileKeyFromTargetInput, gameboardPlanIndex\@gameboard, isTextureSet/isUnitStyle\@catalog, readValidationGameboardPlanFromWorld\@projection, GAMEBOARD\_RELEASE\_GATE\_SUMMARIES + GAMEBOARD\_REQUIRED\_BROWSER\_SCREENSHOT\_ARTIFACTS\@coverage, KAYKIT\_ATTRIBUTION\@schema. * Mechanism: where a shim/barrel does `export *` and must drop internals, either move internals to `src/internal/` (generic) or convert that shim to public-named re-exports (domain-specific). Update all importers + biome noRestrictedImports for `../internal`. typecheck + unit green. * **CONTRACT DECISION (2026-05-28, user: “pick the BEST for the library”):** umbrella = union of legitimately-public subpath symbols; genuinely-internal symbols leave ALL public surfaces. The old hand-list was a curated-subset (umbrella ⊊ subpaths) — that was WRONG for symbols with real external consumers. Classification of the post-barrel-forwarding leaks: * **Move to internal (no external consumer; only cross-file impl detail; auto-typedoc is noise):** `actorRecord`/`placementRecord`/`simulationResult`/`actorTargetsRecordFromReport`/`emptyActorTargetsRecord` (report↔engine) → `src/simulation/internal.ts`; `tryParseHexKey` (coordinates-internal) → `src/internal` or private; `gameboardPlanIndex` (cross-domain layout/interop/gameboard impl) → `src/internal`; `GAMEBOARD_RELEASE_GATE_SUMMARIES` (coverage-internal) + `GAMEBOARD_REQUIRED_BROWSER_SCREENSHOT_ARTIFACTS` (coverage→cli impl) → `src/internal` or interop-internal. * **Keep public — real external consumers, umbrella SHOULD expose (old list wrongly omitted):** `KAYKIT_ATTRIBUTION` (scripts/audit-package.ts) ; `readValidationGameboardPlanFromWorld` (scripts/smoke/\*). These were already public on their subpath; accept into umbrella, add to snapshot. * Final snapshot diff = bootstrap (13) + all genuine internals removed; + KAYKIT\_ATTRIBUTION + readValidationGameboardPlanFromWorld + any other real-consumer symbols added. Delete stale auto-typedoc pages for the now-internal symbols (docs-site reference regen). * **PROGRESS (2026-05-28):** umbrella → barrel-forwarding DONE. src/internal/predicates.ts (5 generic) DONE. src/simulation/internal.ts (SIMULATION\_\* arrays + isSimulation\* guards + tileKeyFromTargetInput) DONE — cycle broken by owning STEP\_ACTIONS in internal.ts. simulation.ts shim curated (report records omitted) DONE. tsc green, 714/715 unit pass, ONLY public-api snapshot red. * **REMAINING 6 leaks + decisions:** KEEP-PUBLIC (real consumers, accept into snapshot): `KAYKIT_ATTRIBUTION` (scripts/audit-package), `readValidationGameboardPlanFromWorld` (scripts/smoke). MOVE-INTERNAL via per-domain shim/barrel curation (same pattern as simulation.ts): `tryParseHexKey` (coordinates.ts-internal, used by parseHexKey), `gameboardPlanIndex` (gameboard.ts, cross-domain → coordinates/interop import it; curate gameboard barrel + give cross-domain callers a path — likely `src/internal` re-home accepting type-only domain imports, OR a gameboard/internal.ts with a biome exception), `GAMEBOARD_RELEASE_GATE_SUMMARIES` (coverage.ts-internal), `GAMEBOARD_REQUIRED_BROWSER_SCREENSHOT_ARTIFACTS` (coverage.ts → cli consumes; cross-domain). Then update snapshot, delete stale typedoc pages, biome rule for any new internal import paths. typecheck + unit + coverage green → commit LF1. **REORDER (2026-05-28, forward self-assessment after LF1):** LF2 (config extraction) was originally first, but the work surfaced that its content — the KAYKIT\_FREE\_GITHUB\_\* constants + repo-clone patterns — is downstream of LF7’s git-clone decision (github→git changes those constants’ semantics entirely) AND lives in bootstrap files that LF4 relocates. Extracting config now would build a schema LF7 then reshapes. BEST order: **LF3 → LF4 → LF7 → LF2 → LF5 → LF6 → LF8** so config captures the SETTLED constants (incl. git-clone patterns) from their final home. (Per “pick the BEST, let work surface the next step”.) ### task-LF2 src/config JSON config domain (reordered: after LF7) [Section titled “task-LF2 src/config JSON config domain (reordered: after LF7)”](#task-lf2-srcconfig-json-config-domain-reordered-after-lf7) * [x] task-LF2 ✅ (commit, 2026-05-28) Created src/config/ with 3 JSON files (kaykit-source, bootstrap-paths, upstream-layouts) + typed loader (`KAYKIT_SOURCE`, `BOOTSTRAP_PATHS`, `UPSTREAM_LAYOUTS`, `kaykitGithubArchiveUrl`). Migrated all KAYKIT\_BOOTSTRAP\_*, KAYKIT\_FREE\_GITHUB\_*, KAYKIT\_FREE\_USER\_AGENT, KAYKIT\_\*\_EXTENSIONS, KAYKIT\_SIDECAR\_SCHEMA\_VERSION, KAYKIT\_MEDIEVAL\_FREE/EXTRA\_LAYOUT to derive from config. Public names unchanged, browser-safe (pure JSON, no node), internal (not in exports). All green. ### task-LF3 Relocate upstream-layout [Section titled “task-LF3 Relocate upstream-layout”](#task-lf3-relocate-upstream-layout) * [x] task-LF3 ✅ (commit, 2026-05-28) Moved src/manifest/upstream-layout.ts → src/bootstrap/upstream-layout.ts (whole file; config extraction of its data deferred to reordered LF2). manifest barrel drops it; bootstrap barrel re-exports. Subpath renamed manifest/upstream-layout → bootstrap/upstream-layout (tsup+package.json+smoke+astro typedoc). 6 layout symbols left umbrella (→ ./bootstrap). Also: react/three added to umbrella (LF1 follow-up, invariant #5, commit `3461858`). All green: typecheck/715 unit/build/lint. ### task-LF4 Relocate bootstrap to CLI command area [Section titled “task-LF4 Relocate bootstrap to CLI command area”](#task-lf4-relocate-bootstrap-to-cli-command-area) * [x] task-LF4 ✅ (commit, 2026-05-28) Moved src/bootstrap/\* → src/cli/commands/bootstrap/ (core/target/upstream-layout/index.ts; index now hosts the `run` command merging the old thin wrapper). \_shared imports `./commands/bootstrap/core` (cycle-safe). tsup + astro typedoc inputs repointed; subpath KEYS + dist paths unchanged. e2e/integration tests repointed. No top-level src/bootstrap/. All green: typecheck/715 unit/build/lint. ### task-LF5 Repoint subpaths + build + lint + docs [Section titled “task-LF5 Repoint subpaths + build + lint + docs”](#task-lf5-repoint-subpaths--build--lint--docs) * [x] task-LF5 ✅ (commit, 2026-05-28) Hand-written docs repointed (architecture, errors guide, kaykit-upstream-layout guide). Auto-typedoc regenerated. Empty stale `reference/bootstrap/` deleted. biome rule needed no change (bootstrap moved INTO src/cli/commands; the rule’s `../bootstrap` matchers no longer apply since there’s no top-level bootstrap domain). package.json exports + tsup + scripts/smoke + astro typedoc inputs already updated in LF3+LF4. All green. ### task-LF6 Proper CLI library [Section titled “task-LF6 Proper CLI library”](#task-lf6-proper-cli-library) * [x] task-LF6 ✅ (commit, 2026-05-28) Adopted citty for command routing + lazy subcommand loading; hand-curated `./usage` preserved for `--help` (intercept BEFORE runMain since citty auto-help only lists subcommand names). All 35 subcommands unchanged (kept run(parsed, sourceRoot, edition) contract; citty wraps each with ctx.rawArgs → parseFlags). Lazy import per command preserves the cold-start budget. 31 cli tests pass; all green. ### task-LF7 Unify bootstrap source on the zip flow (no git, no peer dep) [Section titled “task-LF7 Unify bootstrap source on the zip flow (no git, no peer dep)”](#task-lf7-unify-bootstrap-source-on-the-zip-flow-no-git-no-peer-dep) * [x] task-LF7 ✅ (commit `644862c`, 2026-05-28) Unified: github source downloads the stable `/archive/refs/heads/main.zip` then reuses `stageFromZip` (same detector handles itch.io `/Assets/gltf` + github `-main/addons/kaykit_.../Assets/gltf`). Removed tar/gunzip/stream-pipeline + `tar` runtime dep + `.gz` ext. Verified structures via download. All green. ORIGINAL: **REVISED (user, 2026-05-28):** isomorphic-git rejected as overkill. The GitHub archive URL `https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0/archive/refs/heads/main.zip` is stable and never changes. Collapse the two source modes into ONE flow: the CLI accepts a local zip path and auto-detects free/extra by expected structural alignment (via the layout detector); if no zip is given, it fetches that stable archive .zip and feeds the SAME zip-extract flow (FREE only). Remove the tarball/gunzip/tar machinery (`node:zlib`, `node:stream/promises`, `tar` dep, `kayKitFreeGithubTarballUrl`’s codeload tar.gz URL → archive .zip URL). Keep the existing `openHttpsStream` redirect handling (github archive → codeload redirect) but pipe to a temp `.zip` file then reuse `stageFromZip`. Verify via `tree` that a fresh download of that zip extracts to the same structure as the FREE `references/` tree; support both structures if they differ. Drop `tar` runtime dep. Coverage for the unified path. typecheck+unit+build+lint green. ### task-LF8 Browser-import-safety verification [Section titled “task-LF8 Browser-import-safety verification”](#task-lf8-browser-import-safety-verification) * [x] task-LF8 ✅ (commit, 2026-05-28) Static-graph guard `tests/unit/umbrella-browser-safe.test.ts` walks src/index.ts transitive imports + asserts ZERO `node:*` leaks (72 files visited, 0 leaks). 2026-06-25 empirical verification: browser-free React/Three visual suites load under Chromium, `pnpm test:browser:free` passes with screenshot QA, and `pnpm coverage:all:enforce` measures the merged unit+browser-free tree. ### Phase E9 — visual integration gate (continuation) [Section titled “Phase E9 — visual integration gate (continuation)”](#phase-e9--visual-integration-gate-continuation) * [x] **E9** — ✅ Browser visual integration gate closed (2026-06-25). `tests/browser/free-visual.test.ts`, `simple-rpg-visual.test.ts`, and `feature-gallery.spec.ts` render the Three-backed catalog/gameboard/example surfaces into Chromium and assert committed PNG artifacts under `tests/browser/__screenshots__/`; `tests/browser/react-bindings.test.ts` exercises React hooks/providers/actions in Chromium; `tests/browser/browser-harness-smoke.test.ts` catches Vitest Browser client regressions. Local proof: `pnpm exec tsx src/cli/cli.ts bootstrap --source github --out models`; `pnpm test:browser:free`; `pnpm coverage:all:enforce`. ### Phase F-Site — docs-site continuation [Section titled “Phase F-Site — docs-site continuation”](#phase-f-site--docs-site-continuation) * [x] **F-Audit-7b-equiv** — ✅ Migrated all six `docs/guides/*.md` legacy files into `docs-site/src/content/docs/guides/` with canonical-URL redirect notes pointing back. The legacy `docs/guides/*.md` files stay (load-bearing in `src/scenario/catalog.ts` at lines 1366/1515/1557/1571/1614/1653/1673) with a top-of-file canonical pointer; the docs-site versions are the human-facing canonical with Starlight frontmatter. Six files: guide-scenario-coverage, public-api, recipes-scenarios-and-simulation, release-readiness, rendering-assets-and-external-packs, runtime-integration. ## Phase CR — Comprehensive Review Action Queue (2026-05-28) [Section titled “Phase CR — Comprehensive Review Action Queue (2026-05-28)”](#phase-cr--comprehensive-review-action-queue-2026-05-28) Findings from full 5-phase review (`.full-review/05-final-report.md`). Ordered by priority: P0 must ship before next release; P1 before next release but can batch; P2 sprint-level; P3 backlog. ### P0 — Critical (must fix before next release) [Section titled “P0 — Critical (must fix before next release)”](#p0--critical-must-fix-before-next-release) * [x] **CR-P0-1** — ✅ PR #62 (2026-05-28): main branch protection enabled via `gh api` with required checks: lint/typecheck/build/test/Coverage/Docs Site Build/Semgrep SAST/Dependency Review. * [x] **CR-P0-2** — ✅ PR #62 (2026-05-28): Removed `release-as: "1.0.0"` from `release-please-config.json`. * [x] **CR-P0-3** — ✅ PR #62 (2026-05-28): react/three/koota/react-dom → peerDependencies (optional); @types/react → devDependencies; citty/honeycomb-grid/seedrandom/yauzl stay as runtime deps. * [x] **CR-P0-4** — ✅ (2026-05-28) Added `src/cli/commands/bootstrap/__tests__/security.test.ts` with 6 tests: (a) redirect allowlist — non-allowlisted host rejected, allowlisted host accepted (objects.githubusercontent.com → 503 proves 2nd hop ran), redirect depth >5 rejected; (b) zip-slip — raw-binary zip with `../../../` entry name rejected by extractZipTo; (c) zip-bomb — declared uncompressedSize > 64 MB rejected by pre-check, at-limit (=64 MB) passes pre-check but fails yauzl size-mismatch. Uses `vi.mock('node:https')` + raw zip buffer construction (yazl rejects `../` names). All 6 pass; lint/typecheck/test green. * [x] **CR-P0-5** — ✅ PR #62 (2026-05-28): `readJson` → `readJson(): unknown`; \~25 call sites updated with explicit `as X` casts; two unvalidated plan-return paths (layoutAnalysisPlanFromArgs, summaryPlanFromArgs) now run validateGameboardPlan; null/primitive guards added to readRegistry, readPieceRegistry, pieceOverridesFromArgs. Remaining: `readValidatedJson` wrapper + file-size ceiling + error-contract tests — tracked as CR-P1-partial below. ### P1 — High (fix before next release, can batch) [Section titled “P1 — High (fix before next release, can batch)”](#p1--high-fix-before-next-release-can-batch) * [x] **CR-P1-1** — ✅ (2026-05-29) Added 9 golden-path oracle tests (`pathfinding-oracle.test.ts`) pinning correctness + visit-count ceilings. Replaced O(N) `lowestScoreKey` + Set open-list in `findHexPath` with binary min-heap A\* (lazy deletion via g-cost mismatch). Replaced O(N) `lowestCostKey` in `reachableGameboardTiles` with binary min-heap Dijkstra. Zero new deps. Biome `noNonNullAssertion` satisfied via explicit undefined checks. PR #65. * [x] **CR-P1-2** — ✅ (2026-05-29) Created `src/guides/simple-rpg/` (types/exercises/smoke/index.ts). Moved types + catalog + smoke to production; CLI imports `../guides/simple-rpg` + passes `scenarioJson as GameboardScenario`; test file re-exports production module + zero-arg wrapper. `_shared.ts` no longer imports from `tests/`. PR #66. * [x] **CR-P1-3** — ✅ (2026-05-28) Fixed SAFE\_REF regex in config/index.ts: original `/^[a-zA-Z0-9._\-/]{1,200}$/` allowed `../../etc/passwd` (dots+slashes in charset); replaced with `/^(?!.*\.\.)(?!\.)(?!.*\/$)[a-zA-Z0-9._\-/]{1,200}$/` rejecting `..` sequences, leading dots, trailing `/`. Added CWE-74 guard test in smoke.test.ts; fixed security.test.ts typecheck (3× `as unknown as ReturnType` + headers cast). All 67 test files pass; tsc exit 0; lint clean. * [x] **CR-P1-4** — ✅ (2026-05-29) Added `WeakMap>` for tile-key and placement-id in `koota.ts`. `findTileEntity` + `findPlacementEntity` are now O(1). `spawnGameboardPlan` fills both indexes; `spawnGameboardPlacement` registers; `removeGameboardPlacement` deregisters; `clearGameboardWorld` clears. 68/68 tests pass. * [x] **CR-P1-5** — ✅ (2026-05-29) Added `KNOWN_EXTRA_ASSET_IDS` IIFE-computed `Set` in `catalog.ts`. `isKnownExtraAssetId` is now a single `Set.has()` call. 68/68 tests pass. * [x] **CR-P1-6** — ✅ PR #62 (2026-05-28): dedicated `coverage` job added to ci.yml running `pnpm test:coverage:enforce`; added as required status check in branch protection. * [x] **CR-P1-7** — ✅ (2026-05-28) bootstrap-nightly.yml: SHA-pinned all 4 actions, added pull\_request paths trigger, hoisted HEX\_WORLDS\_OUT\_ROOT to job env. * [x] **CR-P1-8** — ✅ (2026-05-28) engine.ts:663-665: null-check .at(-1) result, throw GameboardRuntimeError with actor.actorId + actor.spawnGroupId. * [x] **CR-P1-9** — ✅ (2026-05-28) release.yml: guarded npm publish with `if: github.event_name == 'release'`. * [x] **CR-P1-10** — ✅ (2026-05-28) core.ts: replaced `dirname(new URL(import.meta.url).pathname)` with `import.meta.dirname`. ### P2 — Medium (plan for next sprint) [Section titled “P2 — Medium (plan for next sprint)”](#p2--medium-plan-for-next-sprint) * [x] **CR-P2-1** — `_shared.ts` decomposition: per-command files; extract `emitOutput()` helper; fix `commandHandlerMutations` to throw `GameboardRuntimeError` on `never`. \[CQ-1, CQ-2] * [x] **CR-P2-2** — ✅ PR #69 (2026-05-29): `findGameboardPath`/`reachableGameboardTiles` default-arg Map allocations routed through `gameboardPlanIndex` WeakMap. * [x] **CR-P2-3** — ✅ PR #69 (2026-05-29): world.query() spreads removed (patrol.ts:227, movement.ts:421); flatMap+spread in runGameboardSystems replaced with nested for-loop push. Koota world.query() verified snapshot via .slice() — gemini live-iterator concern was a false positive. * [x] **CR-P2-4** — ✅ `gameboard.ts` no longer statically imports `freeManifest`; `requiresExtraAsset` uses catalog treatment metadata, `getPlacementAsset` is explicit-manifest sync, and new `loadPlacementAsset` lazy-loads the packaged FREE manifest via dynamic import. Added focused gameboard tests, public API snapshot update, and a `dist/gameboard.js` static import-graph guard proving the FREE manifest record chunk is not statically reachable. Also refreshed packed-consumer smoke peer fixture (`koota`, `three@^0.184.0`) and flat `baseUrl` URL assertion. Verified with `pnpm verify`, `pnpm docs-site:build`, packed-consumer smoke, and `npm pack --dry-run`. * [x] **CR-P2-5** — ✅ `useGameboardDerivedRevision`: split React selector invalidation into state/tile/placement/actor/movement/quest/patrol domains, coalesced trait events through one microtask revision bump, and added jsdom tests proving placement selectors ignore tile-only updates and coalesce repeated placement changes. `pnpm verify` green. \[P-10] * [x] **CR-P2-6** — ✅ `stageUpstreamSource`: inlined github download path with `try/finally` for `downloadRoot` cleanup; `stageFromZip` restructured to `let succeeded = false; try { ... succeeded = true; } finally { if (!succeeded) rmSync(...) }`. * [x] **CR-P2-7** — ✅ `readSidecar`: added `SIDECAR_MAX_BYTES = 4 MiB` + `SIDECAR_MAX_FILES = 100_000` constants; guards throw `GameboardIoError` on oversize or overcount. * [x] **CR-P2-8** — ✅ `walkFilesInternal`: added `symlinkCount` ref parameter; warns once on stderr when symlink encountered; passes through recursive calls. * [x] **CR-P2-9** — ✅ `koota.ts` no longer imports `isKnownExtraAssetId` from `scenario`; runtime placement spawn/update now require callers to set or preserve `requiresExtra` explicitly. Added Koota regression coverage for explicit EXTRA tagging. `pnpm verify` green. \[AR-1] * [x] **CR-P2-10** — ✅ `ci.yml`: unified pnpm/action-setup and actions/setup-node SHAs across all 4 jobs to `v6.0.8` / `v6.4.0` (docs job was on v4.2.0/v6.3.0). * [x] **CR-P2-11** — ✅ `release.yml`: added post-publish `npm audit signatures` verify step; wrote `ROLLBACK.md` runbook covering deprecate, unpublish, patch-release, and signature failure scenarios. * [x] **CR-P2-12** — ✅ `automerge.yml`: branch protection now has strict required checks (`lint`, `typecheck`, `build`, `test`, `Docs Site Build`, `Semgrep SAST`, `Dependency Review`, `Coverage`), so Dependabot keeps `gh pr merge --auto --squash` behind the gate while release-please auto-approval/auto-merge is removed. Release PRs are now a maintainer checkpoint for the computed version + changelog; workflow contract + deployment docs updated. Local proof: `pnpm exec vitest run tests/contract/workflows-contract.test.ts --config vitest.config.ts`; `pnpm typecheck`; `pnpm lint`. \[CI-6] * [x] **CR-P2-13** — ✅ H-DOC-1: fixed CLI reference redundant binary mention; H-DOC-2: added package rename narrative to CHANGELOG.md 1.0.0 + created `docs-site/guides/migration.md`; H-DOC-3: added JSON flag schema/error-contract subsection; M-DOC-1: added `HEX_WORLDS_OUT_ROOT` danger aside. * [x] **CR-P2-14** — ✅ Removed `it.skip` at cli.test.ts:1922 (fixture key updated to `fixture-castle-kit` to satisfy `[A-Za-z0-9_:-]+` guard); tightened `__proto__` guard message assertion in cli-security.test.ts; fixed `--pieces` arg to use temp file (CLI requires path, not inline JSON). Skips at lines 219 (Phase RB) and 1426 (Phase RS) remain legitimately blocked. * [x] **CR-P2-15** — ✅ `requirePlacementState` now returns `Readonly` (live ECS value, no copy) for internal reads; `snapshotPlacementState` does the deep-spread and is used only at the two external-result callsites (requestGameboardMovement and movementAdvanceResult). * [x] **CR-P2-16** — ✅ `tsconfig.json`: removed `ignoreDeprecations: "6.0"`; migrated all `paths` values from bare relative (`src/...`) to `./`-prefixed relative (`./src/...`) and removed `baseUrl: "."` — TS 6 requires explicit relative paths in `paths` when `baseUrl` is absent. tsc clean, all 68 test files pass. * [x] **CR-P2-17** — ✅ `tsup.config.ts`: added `treeshake: true` explicit; documented bundle model (honeycomb-grid/seedrandom/citty/yauzl bundled as deps; koota/react/three external as peerDeps); added `tests/unit/bundle-size.test.ts` with 60KB ceiling on index.js + 100KB ceiling on cli.js (skips when dist/ absent). * [x] **CR-P2-18** — ✅ `release.yml`: pinned `npm@11.11.0` (was `@latest`); added `@cyclonedx/cyclonedx-npm@4.2.1` as pinned devDependency; switched from `npx --yes @cyclonedx/cyclonedx-npm@latest` to `pnpm exec cyclonedx-npm`; updated contract test assertion to match. ### P3 — Backlog [Section titled “P3 — Backlog”](#p3--backlog) * [x] **CR-P3-1** — ✅ Architecture decomposition: `simulation/script.ts` (3,163 lines → script-types/validators/index); `gameboard/gameboard.ts` (2,228 lines → plan/spawn-groups/terrain); `systems/systems.ts` (900 lines → command/tick/events); `scenario/catalog.ts` (2,401 lines). Progress: `simulation/script.ts` is now an 11-line stable public shim over `script-types.ts` (DTOs/schema/result records) and `script-validators.ts` (authored-script validators/scenario index); gameboard plan contracts, builder option contracts, memoized plan index, and plan summary helpers are split into `src/gameboard/plan.ts`; gameboard asset lookup/EXTRA/prop-cluster helpers are split into `src/gameboard/assets.ts`; derived terrain/connectivity placement construction is split into `src/gameboard/terrain.ts`, leaving `gameboard.ts` focused on builder/runtime construction behavior and built-in sample boards; board-aware spawn selection and spawn-group routing are split into `src/gameboard/spawn-groups.ts`; patrol waypoint and route-segment planning is split into `src/gameboard/patrol-routes.ts`, leaving `navigation.ts` focused on occupancy-backed pathfinding/reachability and public navigation re-exports; systems event contracts, event record contracts, and snapshot serializers are split into `src/systems/events.ts`; systems command dispatch contracts/helpers are split into `src/systems/command-dispatch.ts`; systems patrol/movement/quest tick orchestration is split into `src/systems/tick.ts`, leaving `systems.ts` focused on action bundles and command-plus-tick composition; scenario guide-page image/table normalization is split into `src/scenario/catalog-guide-data.ts`, and KayKit public treatment construction is split into `src/scenario/catalog-treatments.ts`, leaving `catalog.ts` focused on public constants, queries, inverse indexes, coverage summaries, and Markdown rendering without moving the public `/catalog` API surface. Local proof: `pnpm typecheck`; focused catalog/public API/docs contract Vitest slice. \[AR-4, AR-5, AR-6] * [x] **CR-P3-2** — ✅ `noRestrictedImports` enforcement gaps: added `../interop/internal`, `../internal/predicates`, trait deep paths, config JSON deep paths, and `.js` mirrors for every extensionless restricted path; routed existing trait imports through `../traits`; added a contract test for required gaps and `.js` mirror coverage. Local proof: `pnpm exec vitest run tests/contract/biome-restricted-imports-contract.test.ts --config vitest.config.ts`; focused actor/Koota/movement/patrol/quest/public-api Vitest slice; `pnpm typecheck`; `pnpm lint`; `pnpm verify`. \[AR-10, BP-8] * [x] **CR-P3-3** — ✅ `interop/coverage.ts` cohesion: documented the release-tooling vs runtime interop split in the architecture page, mirrored the distinction in the `coverage.ts` module comment, and clarified both public API guide copies so `./coverage` is build/review tooling rather than runtime ECS adapter glue. Kept the existing `src/interop/coverage.ts` location because `/coverage` is already a stable public surface joining interop, compatibility, manifest, scenario, and visual evidence. Local proof: `pnpm exec vitest run src/interop/__tests__/coverage.test.ts tests/contract/docs-frontmatter-contract.test.ts --config vitest.config.ts`; `pnpm lint`; `pnpm typecheck`; `pnpm docs-site:build`. \[AR-7] * [x] **CR-P3-4** — ✅ Branded types: added public-api migration status tables to both docs-site canonical and legacy metadata guides, explicitly stating branded IDs are NOT yet enforced; added a docs contract pinning the caveat, tracked domains, tracked brands, and current implementation boundary. \[AR-8, M-DOC-4] * [x] **CR-P3-5** — ✅ `useStableOptions` JSON.stringify: added a plain-empty-options fast-path before serialization; the React memoization test is now included in the main Vitest config and pins that fresh `{}` selector options do not call `JSON.stringify`. Local proof: `pnpm exec vitest run src/react/__tests__/memoization.test.tsx --config vitest.config.ts`. \[P-11] * [x] **CR-P3-6** — ✅ Added `.github/workflows/benchmarks.yml` to run `pnpm build` + `pnpm bench` on main pushes, nightly schedule, manual dispatch, and benchmark-wiring PRs; uploads text benchmark output plus metadata artifacts; workflow contract now pins the benchmark workflow shape and SHA-pinned actions. \[T-bench] * [x] **CR-P3-7** — ✅ Inline docs: added A\* heap/heuristic commentary in `findHexPath`; patrol state-machine transition diagram above `advancePatrolEntity`; section maps for the split simulation script implementation (`script-types.ts` and `script-validators.ts` behind the stable `script.ts` shim); and `docs/` vs `docs-site/` canonical ownership guidance in CONTRIBUTING.md. \[L-DOC-1, L-DOC-2, M-DOC-5, L-DOC-3] * [x] **CR-P3-8** — ✅ `CI_GITHUB_TOKEN` PAT removed from release-please auth. `cd.yml` now mints a repo-scoped GitHub App installation token via pinned `actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1` (v3.2.0), requests only the current repository, narrows App-token permissions to contents/pull-requests write, keeps the job `GITHUB_TOKEN` read-only, and passes `steps.release-please-token.outputs.token` to release-please. Deployment docs name `vars.RELEASE_PLEASE_APP_CLIENT_ID` + `secrets.RELEASE_PLEASE_APP_PRIVATE_KEY`; workflow contract rejects `secrets.CI_GITHUB_TOKEN`. \[CI-9] * [x] **CR-P3-9** — ✅ `GAMEBOARD_REQUIRED_BROWSER_SCREENSHOT_ARTIFACTS` now re-exports through `src/interop/coverage.ts` and the `src/interop/index.ts` barrel. The CLI coverage command imports it from `../../interop` instead of `../../interop/internal`, coverage tests pin the barrel path, and the public API snapshot records the intentional release-tooling export. \[L-1/Sec, AR-2] * [x] **CR-P3-10** — ✅ `advancePatrolEntity` split into explicit transition helpers for inactive, invalid-route, existing-movement, idle/wait/completed, and request-next-segment branches. Trait writes stay in the same behavior points, but the monolithic early-return ladder and mutable `nextAgent` flow are replaced with `PatrolAdvanceTransition` objects. Patrol edge tests, simulation patrol tests, lint, typecheck, and full verify pass locally. \[CQ-7] * [x] **CR-P3-11** — ✅ `hashFile` now hashes through `stream/promises.pipeline` into a hashing `Writable`, so verification waits for stream cleanup instead of resolving on the read stream `end` event. Bootstrap core tests, lint, typecheck, and full verify pass locally. \[CQ-9] * [x] **CR-P3-12** — ✅ Removed the dead `src/simulation/simulation.ts` double-shim and moved the public simulation barrel directly into `src/simulation/index.ts`. Source docs now point at `src/simulation/index.ts`; focused simulation/public API tests, lint, typecheck, and full verify pass locally. \[AR-3] # RFC-0001 render surface — build log (in progress) > Progress record for RFC-0001 (generic asset sources + the declarative render surface). Migrated out of the live directive so the queue tracks only remaining work. Completed through the JSX element layer + docs-site workspace member; RFC-0001 is still open (one giant PR > **Live milestone — build log, not an archive.** RFC-0001 is still in flight on `feat/generic-asset-sources` (PR #220, the one-giant-PR accumulator). This page holds the per-commit record for the COMPLETED portion (workspace, G0, RFC0-7, RFC0-8 + element layer, docs-site member) so `.agent-state/directive.md` carries only the remaining `[ ]` queue. Spec of record: `packages/declarative-hex-worlds/docs/rfcs/0001-generic-asset-sources.md`. ## Completed items (verified: local green + CI green) [Section titled “Completed items (verified: local green + CI green)”](#completed-items-verified-local-green--ci-green) * [x] RFC0-7 `AssetSource` interface; extract KayKit `gltf-pack` as the first impl behind it (pure refactor, existing GLTF tests are the net). ✅ commit b06c7bb: `src/asset-source/source.ts` (AssetSource iface + AssetRenderRequest union ‘gltf’|‘tileset-cell’ + ResolveContext + CellRect/HexDims, type-only → coverage-excluded), `gltf-pack.ts` (createGltfPackSource wraps resolveGameboardPlacementAssetUrl+transformForPlacement → {type:‘gltf’}, 100% covered, 7 tests). Design: docs/plans/declarative-render-surface.design.md. Umbrella + ./asset-source + public-api snapshot updated. Suite 2674 pass. * [x] RFC0-8 `./tileset` + declarative render surface — DONE. Tileset manifest + UV-cell math + textured-hex mesh + bridge dispatch + the full JSX element surface, all CI-green. (SimpleRPG tileset render mode + zero-config wrapper are follow-ons under RFC0-2/showcase work.) * ✅ G2 manifest + cell selection DONE: commit 147cf8a (TilesetManifest Zod schema, 8 tests) + 08abd2d (createTilesetSource: biome/edge→tileset-cell, FNV-1a hash fill selection, cellRect helper, 15 tests). Both 100% covered, in `src/asset-source/`. * ✅ buildTexturedHexMesh DONE: commit 61487f3. `src/three/textured-hex.ts` — buildHexGeometry (center+6-corner fan BufferGeometry, pointy-top default, per-cell UVs, V-flipped) + buildTexturedHexMesh (MeshBasicMaterial, transparent, DoubleSide). Pure three, unit-covered, 10 tests, 100%. NOTE geometry math: pointy-top X-extent = halfW·cos30°, Z-extent = halfH (corner at 90°). * ✅ BRIDGE DISPATCH DONE: commit 61f6076. src/three/three.ts loadGameboardPlacementObject now dispatches on an optional `source?: AssetSource`: tileset-cell → loadTilesetCellObject (GameboardSheetTextureLoader injectable+LRU-cached via loadSheetTextureCached → buildTexturedHexMesh → transform → tag → LoadedGameboardPlacementObject{mesh, clips:\[]}); gltf/no-source → unchanged GLTF path. 5 unit tests + tests/browser/tileset-render.test.ts (3×3 board in real Chromium/WebGL, canvas-backed sheet, asserts 9 meshes + non-empty pixels — covers three.ts dispatch for the merged strict-100 gate; registered in vitest.browser.free.config.ts). No import cycle (type-only asset-source↔three). * ✅ JSX SURFACE (RFC0-8b) DONE: `src/react-elements/` + `./react-elements` subpath (subpath-only, NOT umbrella — keeps @react-three/fiber an optional peer for consumers who don’t want the elements). `` (Canvas-free: koota providers + source registry), `` (R3F bridge — useFrame → the extracted pure syncHexWorldPlacements), ``/``/`` (spawn/remove koota placements on mount/unmount), `` (registers a tileset source), + hooks useHexWorld/useTile/usePlacement/ useHexPath. combineSources (first-match composite) + syncHexWorldPlacements extracted as pure fns → unit-tested (src/react-elements/**tests**/objects.test.ts, 8 tests); the R3F component wrapper is /\* v8 ignore \*/ (untraceable through R3F’s reconciler, logic covered). Element logic browser-tested via plain createRoot (tests/browser/react-elements.test.ts, 13 tests — NOT inside , since only needs R3F; that’s a Canvas smoke). Needed vitest.browser config: React dedup + @react-three/fiber in optimizeDeps (R3F’s own reconciler was loading a 2nd React → useMemo null). All files 100% covered, full suite 2723 pass, build+DTS green. FOLLOW-ONS (not blocking RFC0-8): useSelection + useCamera hooks land with their backing features (a selection-state model + RFC0-CAM); zero-config wrapper; SimpleRPG tileset render mode (RFC0-2). **Decision:** is Canvas-free — the consumer owns the R3F , dhw owns the board composition + render-sync bridge inside it. **Why:** Canvas/camera/renderer/lights/post are app+art decisions (burden side of the boon/burden line); owning them would presume and constrain. A Canvas-free primitive composes with consumer R3F content and stays testable. Optional covers zero-config. **Resolves:** RFC0-8b element-layer architecture (Canvas ownership). — resume state + gap finding — ### RFC-0001 resume state (2026-07-06, after G0) [Section titled “RFC-0001 resume state (2026-07-06, after G0)”](#rfc-0001-resume-state-2026-07-06-after-g0) **Branch:** feat/generic-asset-sources, PR #220 (draft). CI green on RFC0-1 + G0 (build/lint/typecheck/coverage/bootstrap/benchmarks pass; CodeQL is GitHub default-setup = neutral/flaky, NOT a real gate). **DONE + verified (local green + CI green):** * RFC0-1 workspace move (library → packages/declarative-hex-worlds, private root, only lib publishes, native pnpm caching CI, release-please retargeted). * RFC0-G0 Zod AssetSourceSpec (src/asset-source/, ./asset-source subpath) + free.ts retirement (16.5k→41 lines) + pnpm catalog + zod dep. 2662 tests pass. * docs-site adopted as `packages/docs-site` workspace member (commit c619ec0) — the STRUCTURAL PREREQUISITE for RFC0-4’s live SimpleRPG React island: only a member can `workspace:*`-depend on the library + `@declarative-hex-worlds/simple-rpg`. pnpm typedoc-plugin-markdown strict-isolation failure solved via root `.npmrc` (`hoist-pattern[]=*` to keep defaults + `*typedoc-plugin*` + `starlight-typedoc`). astro/tsconfig.typedoc entryPoints repointed to sibling lib; cli-reference + docs-frontmatter/branded-types contract tests repointed under packages/docs-site; ci/cd docs-site jobs collapsed to one workspace install. 2683 tests pass; docs build 1194 pages; frozen install clean. (RFC0-4 showcase-CAPTURE still open — waits on RFC0-8 declarative surface + RFC0-2 SimpleRPG render.) **NEXT — RFC0-2 (SimpleRPG package), a large mechanical move:** * packages/simple-rpg/ is SCAFFOLDED (package.json private workspace:\* dep, tsconfig with jsx). Empty src/ — needs the actual move. * MOVE src/guides/simple-rpg/{smoke,exercises,types,index}.ts → packages/simple-rpg/src/ (these import the library by PACKAGE NAME already — clean). * MOVE the scattered SimpleRPG tests into packages/simple-rpg/tests/: tests/integration/simple-rpg/*, tests/e2e/simple-rpg*, tests/browser/simple-rpg-visual.test.ts, tests/unit/simple-rpg.test.ts, tests/simple-rpg/\*. REPOINT their relative imports (../../src, ../../../src/guides/simple-rpg) to the package name ‘declarative-hex-worlds’ + local paths. * The library’s src/guides/simple-rpg/**tests**/{smoke,exercises}.test.ts move too. * Add packages/simple-rpg/vitest.config.ts; ensure `pnpm --filter @declarative-hex-worlds/simple-rpg test` green. * Update the library’s tsconfig include/exclude + any contract test that counted simple-rpg files. * CI: add a layered simple-rpg e2e job that runs AFTER the library suite (needs:), per D-test-topology. * Give SimpleRPG an R3F render surface + run states (compose/cross-pack/pathfind/viewport) — this is where the real gap-finding + showcase capture begins (RFC0-4). **THEN (Phase 2):** RFC0-7 AssetSource interface (build on G0) → RFC0-8 ./tileset + declarative /// elements + hooks → RFC0-CAM camera → RFC0-CLI binder + web configurator → RFC0-TEX/TAG/NORM/OVERLAY/ACC (CC0-pack gaps) → RFC0-10 three downloadable KayKit defaults → Phase 1 showcase backbone swap → RFC0-14 little-legends re-homing. See RFC 0001 for the full design. ### Gap-finding result (2026-07-06): NO declarative render surface exists yet [Section titled “Gap-finding result (2026-07-06): NO declarative render surface exists yet”](#gap-finding-result-2026-07-06-no-declarative-render-surface-exists-yet) SimpleRPG’s first render attempt confirms the RFC thesis gap: the library ships React PROVIDERS + HOOKS (GameboardRuntimeProvider, useGameboardState, useGameboardPlacementSnapshots, action hooks) but NO ready-to-use board React component — no ///. A consumer must hand-wire R3F * syncGameboardPlacementObjects (the imperative three bridge) themselves — exactly what little-legends’ HexBoard did. So SimpleRPG can’t render declaratively until RFC0-8 (the /// + elements + hooks that proxy koota+honeycomb+three) exists. EFFECTIVE ORDER: build RFC0-8 declarative elements NEXT (the core deliverable per the React thesis), with SimpleRPG as their first consumer + gap-finder. The imperative bridge becomes the internal engine under the declarative surface. ## RFC0-CORE — the `./core` koota-free + three-free tier (complete) [Section titled “RFC0-CORE — the ./core koota-free + three-free tier (complete)”](#rfc0-core--the-core-koota-free--three-free-tier-complete) The “declare + JSON + validate + hex math + interop, bring-your-own runtime/renderer” tier. Three source splits + an isolated build + a source-and-built purity contract. * **Recipe split** (commit 237874f): `recipe.ts`’s only koota use — `applyGameboardRecipeGeneration` (seeded layout-fill via a koota world) — moved to `src/scenario/recipe-generation.ts`. `createGameboardPlanFromRecipe` gained an injectable `applyGeneration` param defaulting to a module-level applier: `pureRecipeGenerationApplier` (koota-free; passes no-fill-rule generation through, throws a clear “requires the runtime tier” if fill rules present). The runtime tier wires the koota applier as the default on import (side-effect via the scenario barrel), so all \~10 callers keep today’s behavior. recipe.ts → zero koota. * **Layout split** (commit 0ac4f4f): layout.ts’s 2 koota-spawning exports (`spawnGameboardLayoutPlacements`, `spawnGameboardLayoutFill`) + `projectWorldForLayout` moved to `src/coordinates/layout-runtime.ts`; the 10 pure layout fns stay. projection.ts needed NO split — all 3 exports take a World (runtime-only module `./core` omits). * **`./core` barrel + subpath** (commit a87f180): `src/core/index.ts` re-exports the pure modules directly (bypassing the runtime-mixed barrels), `./core` subpath added. * **Isolated build** (commit 205f7de): tsup.config is now an array — main build (splitting:true, for koota trait-identity stability) + a standalone `core` build (splitting:false), so `dist/core.js` never shares a koota-importing chunk. Verified: the built core imports only honeycomb-grid + seedrandom + zod; `import('./dist/core.js')` loads 112 exports in a koota-less context. * **Contract**: `tests/contract/core-tier-purity-contract.test.ts` asserts BOTH the source import graph AND the built dist/core.js are koota/three/react-free (4 tests, regression-locked). # State > Current published version, in-flight initiatives, links to PRD + directive. ## Current state (pre-1.0) [Section titled “Current state (pre-1.0)”](#current-state-pre-10) The library is pre-1.0. The published npm version is below the major-version stability boundary; APIs can move. The 1.0 stabilization PR (`codex/1.0-stabilization-phase-2`) closes the gap to 1.0 by working through 4 phases: * **Phase R** — restructure (de-monorepo, decompose `src/` into 20 sub-packages, co-locate tests, drop bundled assets). * **Phase A–E** — foundation gates (lint, typecheck, audit, coverage), perf criticals, security criticals, architectural debt, test debt to 100/100/100/100. * **Phase F** — documentation (Astro Starlight site at [docs-site](/), SimpleRPG-driven feature gallery, marketing README, full audit). * **Phase G** — release readiness (SLSA L3, SBOM, version bump, npm publish). ## In-flight initiatives [Section titled “In-flight initiatives”](#in-flight-initiatives) The authoritative work queue is [`.agent-state/directive.md`](https://github.com/jbcom/declarative-hex-worlds/blob/main/.agent-state/directive.md). Open items at a glance: * **Asset bootstrap rollout** — `bootstrapKayKitAssets()` ships; bringing the FREE browser visuals CI job out of `RUN_BROWSER_VISUALS` gating once the bootstrap step is wired into CI. * **SimpleRPG `game/` decomposition** — the test-driver implementation grows from a single 1,005-line file into per-domain sibling modules (scenarios, pieces, systems, render, cli) to expose every public API. * **Feature gallery** — `docs-site/src/content/docs/features/` fills with screenshot-driven pages (harbors, bridges, multi-depth, prop injection, cross-kit composition, determinism) produced by SimpleRPG. * **Marketing README rebuild** — replaces the current feature-enumeration with a 3-screenshot hero strip + 30-line quickstart + “why this exists” pitch. * **Coverage ratchet to 100%** — the E0-E10 sub-epic closes the remaining \~35% gap between the current floor and the PRD §6 invariant of 100/100/100/100. ## Reference [Section titled “Reference”](#reference) | | Where | | --------------------- | -------------------------------------------------------------- | | PRD 1.0 | `docs/PRD/1.0.md` in the repo | | Work queue | `.agent-state/directive.md` | | Test trinity overview | [`/guides/testing/`](/guides/testing/) | | Release flow | [`/about/deployment/`](/about/deployment/) | | Architecture | [`/about/architecture/`](/about/architecture/) | | Design pitch | [`/about/design/`](/about/design/) | | API reference | [`/reference/`](/reference/) (1107 pages generated from JSDoc) | # Features > Screenshot-driven feature gallery of declarative-hex-worlds. import { Aside } from ‘@astrojs/starlight/components’; The gallery shows every facet of the library a prospective consumer needs to understand. Each page is structured the same way: a problem statement, a minimal TypeScript snippet, the library API cross-links, related features. Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate (PRD F-Gallery-1). The prose + snippets compile + run today. ## Gallery [Section titled “Gallery”](#gallery) | Feature | What it shows | | ----------------------------------------------------------- | ------------------------------------------------------------------- | | [Harbors](/features/harbors/) | Fixed gameboard composition with water + piers + village settlement | | [Bridges and connectors](/features/bridges-and-connectors/) | Procedural seed-stable bridges across rivers | | [Multi-depth stacks](/features/multi-depth-stacks/) | Cliffs, plateaus, layered biomes via per-tile depth | | [Tile injection](/features/tile-injection/) | Mutate tile state at runtime via commands | | [Prop injection](/features/prop-injection/) | Add decorative or interactive props after build | | [Pieces and actors](/features/pieces-and-actors/) | Player + NPCs + props with team / hostility / interaction metadata | | [Movement and patrols](/features/movement-and-patrols/) | Cost-aware pathing + scripted patrol routes | | [Quests](/features/quests/) | Multi-objective quests with progress + completion events | | [Cross-kit composition](/features/cross-kit-composition/) | Mix KayKit Hexagon + Adventurers + Kenney assets | | [Determinism replay](/features/determinism-replay/) | Save seed + script → reproduce exact world + simulation anywhere | # Bridges and connectors > Procedurally bridge rivers and ravines with seed-stable connector tiles. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) A river splits your board into two halves and you want the procedural generator to lay down two or three bridges at sensible spans (not too close, not over impassable terrain) every time the same seed runs. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { createSeededGameboardPlan } from 'declarative-hex-worlds/rules'; const plan = createSeededGameboardPlan({ seed: 'forest-with-river-3', shape: { kind: 'rectangle', width: 12, height: 8 }, generators: [ { kind: 'river', source: { q: 0, r: 3 }, sink: { q: 11, r: 5 } }, { kind: 'bridges', maxBridges: 3, minSpacing: 3 }, ], }); // Same seed → same river path → same bridges. Always. ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **River pathing.** A\* between source + sink with terrain-cost weighting. * **Bridge placement.** Seed-aware sampler picks span sites that satisfy `minSpacing` and don’t cross unrenderable terrain. * **Determinism contract.** [See the determinism guide.](/guides/determinism/) ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`createSeededGameboardPlan`](/reference/rules/) * [`GameboardLayoutArchetype`](/reference/layout/) ## Related features [Section titled “Related features”](#related-features) * [Harbors](/features/harbors/) * [Multi-depth stacks](/features/multi-depth-stacks/) # Cross-kit composition > Mix KayKit Medieval Hexagon tiles with KayKit Adventurers characters and other GLTF packs. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) You want KayKit Medieval Hexagon Pack tiles for the gameboard, but characters from KayKit Adventurers 2.0 (FREE) for the player + NPCs, plus a tower asset from Kenney’s Castle Kit as a special landmark. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { analyzeExternalAssetCompatibility, declareGameboardPiecesFromCompatibilityReports, createGameboardPieceRegistry, createGameboardPieceSourceUrlMap, } from 'declarative-hex-worlds'; const knightReport = await analyzeExternalAssetCompatibility({ assetUrl: '/assets/adventurers/knight.gltf', intendedRole: 'unit', }); const pieces = declareGameboardPiecesFromCompatibilityReports([ { name: 'adventurer:knight', report: knightReport, role: 'unit' }, ]); const registry = createGameboardPieceRegistry(pieces); const urlByAssetId = createGameboardPieceSourceUrlMap(registry, { sourceRoots: { 'adventurers': '/assets/adventurers' }, }); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **GLTF inspection.** Reads bounds, rig presence, animation clips, material slots from any GLTF; reports placement compatibility. * **Piece declaration.** Wraps the report into a `GameboardPieceDeclaration` that fits the manifest schema. * **URL resolution.** Per-source-pack URL maps so the renderer knows where to load each cross-kit asset from. * **Size normalization.** [`normalizeAssetToCell`](/reference/normalize/) fits a different maker’s asset — even a different hex orientation — into the target cell, and [`overlayTransform`](/reference/overlay/) places it. The examples package bakes a few CC0 **Kenney Hexagon Kit** pieces (flat-top hexes, a different aspect than KayKit’s pointy-top cell) and a node test proves they normalize + overlay through these same seams — cross-maker extension, not just cross-kit within KayKit. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`analyzeExternalAssetCompatibility`](/reference/interop/) * [`declareGameboardPiecesFromCompatibilityReports`](/reference/pieces/) * [`createGameboardPieceSourceUrlMap`](/reference/pieces/) ## Related features [Section titled “Related features”](#related-features) * [Pieces and actors](/features/pieces-and-actors/) * [Asset bootstrap](/guides/asset-bootstrap/) # Determinism replay > Save the seed + script, reproduce the exact same world + simulation across every machine. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) Your game has a “share replay” button. The user records 5 minutes of gameplay; you want any other player on any machine to reproduce that 5 minutes exactly — same world layout, same NPC paths, same combat outcomes — by loading just the seed + the input script. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { createGameboardRuntimeFromScenario } from 'declarative-hex-worlds/runtime'; import { runGameboardScenarioSimulationScript } from 'declarative-hex-worlds/simulation'; const runtime = createGameboardRuntimeFromScenario(scenario); const result = runGameboardScenarioSimulationScript(scenario, replayScript, { rounds: 300, }); // Same scenario + same script always produces the same `result.events`. // `result.events` is byte-identical to what the recorder produced. ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Seed threading.** Every random decision routes through `seedrandom`; same seed → same outputs. * **Script replay.** `runGameboardScenarioSimulationScript` executes recorded commands in order; the event records become a fingerprint. * **Cross-process gate.** PRD E1’s `tests/unit/determinism.test.ts` spawns N subprocesses + asserts byte-identity. The gate runs in default `pnpm test` — regressions can’t sneak in. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`createSeededGameboardPlan`](/reference/rules/) * [`runGameboardScenarioSimulationScript`](/reference/simulation/) * [Determinism contract guide](/guides/determinism/) ## Related features [Section titled “Related features”](#related-features) * [Quests](/features/quests/) * [Movement and patrols](/features/movement-and-patrols/) # Harbors > Compose a fixed harbor with water tiles, piers, and a small village settlement. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. The snippet below compiles + runs today. ## Problem [Section titled “Problem”](#problem) You want a hex board where a stretch of coastline opens into a deep-water harbor with two wooden piers and a few boats at anchor. The terrain should validate (no land tiles inside the water cluster, piers spanning the shore, navigable tiles for actors). ## Snippet [Section titled “Snippet”](#snippet) ```tsx import { createGameboardBuilder } from 'declarative-hex-worlds/gameboard'; const plan = createGameboardBuilder({ seed: 'harbor-village-7', shape: { kind: 'rectangle', width: 8, height: 6 }, }) .addHarbor({ center: { q: 4, r: 2 }, radius: 2 }) .addBridge({ from: { q: 3, r: 1 }, to: { q: 4, r: 1 } }) .addBuilding({ at: { q: 1, r: 4 }, kind: 'tavern' }) .addBuilding({ at: { q: 2, r: 4 }, kind: 'home_A' }) .build(); // plan.tiles + plan.placements are deterministic — same seed, same harbor. ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Tile selection.** `addHarbor` picks water + coast variants from the FREE manifest, rotates them to match neighbouring tile edges. * **Connectivity validation.** The bridge between `{q:3,r:1}` and `{q:4,r:1}` is rejected if those tiles aren’t adjacent. * **Determinism.** Same seed → same harbor layout across every machine. * **Placement metadata.** Every placement has a `metadata.layoutFootprintSize` + `metadata.pieceRole` for downstream rendering. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`createGameboardBuilder`](/reference/gameboard/) — the entry point. * [`GameboardBuilder.addHarbor`](/reference/gameboard/) — harbor placement. * [`GameboardBuilder.addBridge`](/reference/gameboard/) — connector placement. * [`projectWorldToGameboardPlan`](/reference/coordinates/) — once you’ve spawned the plan into a koota world, project back to a `GameboardPlan` for serialization. ## Related features [Section titled “Related features”](#related-features) * [Bridges and connectors](/features/bridges-and-connectors/) * [Multi-depth stacks](/features/multi-depth-stacks/) * [Cross-kit composition](/features/cross-kit-composition/) # Movement and patrols > Tile-to-tile movement with cost-aware pathing + scripted patrol routes for NPCs. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) A guard NPC patrols a fixed route around the village walls. You want the patrol to step one tile per simulation tick, pause for 3 ticks at each waypoint, and reverse at the end of the loop. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { GameboardPatrolAgent } from 'declarative-hex-worlds/patrol'; world.add(guardEntity, GameboardPatrolAgent({ routeId: 'wall-loop', waypoints: ['2,2', '2,4', '4,4', '4,2'], pauseTicksPerWaypoint: 3, direction: 'forward', onLoopEnd: 'reverse', })); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Per-tick stepping.** `runGameboardSystems` advances every patrol agent one tile (or pauses) per tick. * **Blocked-route fallbacks.** If the next waypoint becomes blocked, the agent waits + emits a `GameboardPatrolBlockedEvent`. * **Event records.** Every step / wait / blocked event is captured in the simulation report for testing + replay. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`GameboardPatrolAgent`](/reference/traits/) * [`runGameboardSystems`](/reference/systems/) ## Related features [Section titled “Related features”](#related-features) * [Pieces and actors](/features/pieces-and-actors/) * [Quests](/features/quests/) # Multi-depth stacks > Stack hex tiles at varying Y depths to build cliffs, plateaus, and layered biomes. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) You want a cliff face: lower coastal hexes at depth 0, a sheer cliff at depth 1, and a plateau of grass + trees at depth 2. Visitors should be able to spawn on each tier; AI pathing should respect the elevation deltas. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { createGameboardBuilder } from 'declarative-hex-worlds/gameboard'; const plan = createGameboardBuilder({ seed: 'cliff-3', shape: { kind: 'rectangle', width: 6, height: 6 }, }) .addTier({ at: { q: 1, r: 1 }, depth: 0, terrain: 'coast' }) .addTier({ at: { q: 2, r: 1 }, depth: 1, terrain: 'cliff' }) .addTier({ at: { q: 3, r: 1 }, depth: 2, terrain: 'grass' }) .addNaturePlacement({ at: { q: 3, r: 1 }, kind: 'tree_single_A' }) .build(); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **HexTileState.depth.** Per-tile Y offset; the projection pipeline reads it for world-space height. * **Stack rules.** Adjacent tile depth deltas above the configured threshold fail validation. * **Navigation.** `createGameboardActorNavigationProfile` respects elevation cost when planning paths. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`HexTileState`](/reference/traits/) * [`projectWorldToGameboardPlan`](/reference/coordinates/) * [`createGameboardActorNavigationProfile`](/reference/actors/) ## Related features [Section titled “Related features”](#related-features) * [Harbors](/features/harbors/) * [Bridges and connectors](/features/bridges-and-connectors/) # Pieces and actors > Spawn the player, NPCs, and neutral actors with team/hostility/interaction metadata. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) You need a player character on the harbor tile, a tavern keeper inside the tavern, and three hostile bandits camped on the road. Each has a different role flag + collision profile + interaction behavior. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { registerGameboardActor, spawnGameboardActor } from 'declarative-hex-worlds/actors'; const player = registerGameboardActor(world, { id: 'player:you', kind: 'player', tileKey: '4,2', team: 'players', }); const tavernKeeper = registerGameboardActor(world, { id: 'npc:keeper', kind: 'npc', tileKey: '1,4', team: 'neutrals', }); const bandit = registerGameboardActor(world, { id: 'enemy:bandit-1', kind: 'npc', tileKey: '5,3', team: 'bandits', hostility: { towards: ['players', 'neutrals'] }, }); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Trait composition.** Each actor gets `GameboardActor` + (`IsPlayerActor` | `IsNpcActor` | `IsPropActor`) + (`IsHostileActor` if applicable) traits. * **Team-based queries.** `HostileActorQuery` returns just the bandits relative to the player. * **Tile occupancy.** Spawn refuses to place an actor on a tile that has a blocking placement. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`registerGameboardActor`](/reference/actors/) * [`GameboardActor`](/reference/traits/) * [`HostileActorQuery`](/reference/actors/) ## Related features [Section titled “Related features”](#related-features) * [Movement and patrols](/features/movement-and-patrols/) * [Quests](/features/quests/) # Prop injection > Attach decorative or interactive props to existing tiles after the board has rendered. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) A merchant arrives at the tavern tile with three crates of trade goods. You want the crates rendered on the tile, queryable by AI, and removable when the merchant leaves. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { spawnGameboardPlacement } from 'declarative-hex-worlds/koota'; const crateA = spawnGameboardPlacement(world, { tileKey: '2,4', assetId: 'crate_A_big', kind: 'prop', layer: 'prop', metadata: { ownerActorId: 'merchant:trader-7' }, }); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Placement spawn.** Adds the koota entity with `IsGameboardPlacement` + `PlacementOnTile` + `PlacementState` traits. * **Footprint check.** Asserts the prop fits within the tile’s available footprint slots. * **Query surface.** Consumer can later `findGameboardActor(world, ...)` to locate the merchant + iterate their placements. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`spawnGameboardPlacement`](/reference/koota/) * [`PlacementState`](/reference/traits/) ## Related features [Section titled “Related features”](#related-features) * [Tile injection](/features/tile-injection/) * [Pieces and actors](/features/pieces-and-actors/) # Quests > Multi-objective quests with progress tracking, completion events, and replay-safe state. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) A “Clear the harbor” quest has three objectives: defeat 5 bandits, deliver 3 crates to the tavern, talk to the harbormaster. The quest tracks each objective independently + fires a completion event when all three resolve. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { GameboardQuest } from 'declarative-hex-worlds/quests'; world.spawn(GameboardQuest({ id: 'quest:clear-harbor', title: 'Clear the harbor', objectives: [ { id: 'defeat-bandits', kind: 'defeat-count', target: 5, current: 0 }, { id: 'deliver-crates', kind: 'deliver-count', target: 3, current: 0 }, { id: 'talk-harbormaster', kind: 'interact-actor', target: 'npc:harbormaster' }, ], rewards: { gold: 100, xp: 250 }, })); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Objective tracking.** Each kind has its own update hook that runs during the simulation tick. * **Completion semantics.** A quest completes when every objective’s `status === 'satisfied'`. * **Event records.** `GameboardQuestAdvancedEvent` / `GameboardQuestCompletedEvent` / `GameboardQuestBlockedEvent` for downstream UIs. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`GameboardQuest`](/reference/traits/) * [`GameboardQuestObjectiveProgress`](/reference/quests/) ## Related features [Section titled “Related features”](#related-features) * [Pieces and actors](/features/pieces-and-actors/) * [Movement and patrols](/features/movement-and-patrols/) # Tile injection > Mutate tile state after the board is built — terrain swaps, depth lifts, connectivity rewires. import { Aside } from ‘@astrojs/starlight/components’; Screenshot embedding lands once the F-Gallery test harness flips on with the RB browser-CI gate. ## Problem [Section titled “Problem”](#problem) A river runs through your board. At runtime a quest objective dams the river upstream; downstream tiles need to switch from `water` to `mud` for the next 50 simulation ticks, then revert. ## Snippet [Section titled “Snippet”](#snippet) ```ts import { gameboardCommandActions } from 'declarative-hex-worlds/commands'; import { HexTileState } from 'declarative-hex-worlds/traits'; const swap = world.actions(gameboardCommandActions).plan({ kind: 'inject-tile', tileKey: '4,2', patch: { terrain: 'mud' }, }); world.actions(gameboardCommandActions).execute(swap); // Later, revert. world.actions(gameboardCommandActions).execute({ ...swap, patch: { terrain: 'water' }, }); ``` ## What the library handles [Section titled “What the library handles”](#what-the-library-handles) * **Trait mutation under koota’s transaction model.** Tile state changes are observable by queries. * **Re-validation.** Connectivity rules re-run; downstream placements that depended on `water` neighbors may flag warnings. * **Snapshot stability.** Post-mutation `runtime.snapshot()` reflects the new state; deterministic replay from the same script reproduces the same sequence. ## API cross-links [Section titled “API cross-links”](#api-cross-links) * [`gameboardCommandActions`](/reference/commands/) * [`HexTileState`](/reference/traits/) ## Related features [Section titled “Related features”](#related-features) * [Prop injection](/features/prop-injection/) * [Pieces and actors](/features/pieces-and-actors/)