RFC-0001 render surface — build log (in progress)
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.mdcarries 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)”- RFC0-7
AssetSourceinterface; extract KayKitgltf-packas 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. - 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-elementssubpath (subpath-only, NOT umbrella — keeps @react-three/fiber an optional peer for consumers who don’t want the elements).<HexWorld>(Canvas-free: koota providers + source registry),<GameboardObjects>(R3F bridge — useFrame → the extracted pure syncHexWorldPlacements),<Tile>/<Model>/<Sprite>(spawn/remove koota placements on mount/unmount),<Tileset>(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
- ✅ 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
Decision:
RFC-0001 resume state (2026-07-06, after G0)
Section titled “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-siteworkspace member (commit c619ec0) — the STRUCTURAL PREREQUISITE for RFC0-4’s live SimpleRPG React island: only a member canworkspace:*-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 testgreen. - 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
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”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
- 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)”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 tosrc/scenario/recipe-generation.ts.createGameboardPlanFromRecipegained an injectableapplyGenerationparam 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) +projectWorldForLayoutmoved tosrc/coordinates/layout-runtime.ts; the 10 pure layout fns stay. projection.ts needed NO split — all 3 exports take a World (runtime-only module./coreomits). ./corebarrel + subpath (commit a87f180):src/core/index.tsre-exports the pure modules directly (bypassing the runtime-mixed barrels),./coresubpath added.- Isolated build (commit 205f7de): tsup.config is now an array — main build
(splitting:true, for koota trait-identity stability) + a standalone
corebuild (splitting:false), sodist/core.jsnever 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.tsasserts BOTH the source import graph AND the built dist/core.js are koota/three/react-free (4 tests, regression-locked).