SLOPSHOPPER

olo-read-gates

Holds OloEngine's read-before-edit rules: the shader, component and test guides before those edits; no edits to vendor, vcpkg or generated files; new test…

newguard
★ 17v0.1.0NOASSERTIONupdated 2026-10-10drsnuggles8/OloEngineBase/.claude/mods/olo-read-gates
A shopper browsing a rack in a slop shop
README

OloEngine

Windows Sanitizers GPU Conformance (AMD, self-hosted) Cross-Vendor Conformance Fuzz Smoke SonarCloud Scan Pre-commit checks

OloEngine is a cross-platform (Windows / Linux) C++23 interactive-application and rendering engine. It started as a follow-along of Hazel but has since grown well past that scope into a full ECS-driven engine with a physically-based OpenGL 4.6 renderer, 2D/3D physics, dual C#/Lua scripting, networking, navigation, procedural terrain and environmental systems, a gameplay/AI stack, and an ImGui editor.

Screenshots

Metallic and dielectric PBR spheres (a roughness sweep) reflecting a procedural star-nest sky:

PBR materials reflecting a procedural sky

PBR modelAnimated models3D physicsFFT ocean
PBR modelAnimated models3D physicsFFT ocean

Table of contents

Repository layout

Target / directoryWhat it is
OloEngine/The engine static library (OloEngine). All subsystems live under src/OloEngine/<Subsystem>/; platform glue under src/Platform/.
OloEditor/ImGui-based editor (OloEditor). Panels under src/, runtime assets under assets/, a sample game under SandboxProject/.
OloRuntime/Standalone game runtime (OloRuntime) that loads what the editor builds.
OloServer/Headless dedicated server (OloServer) — the one target that also runs on WSL2.
OloEngine-ScriptCore/C# scripting API library, built with dotnet build under any generator; C# scripting runs on Windows only.
OloEngine-LuaScriptCore/Lua scripting core (all platforms).
OloEngine/tests/GoogleTest suite (OloEngine-Tests).
tools/Build-time tooling, notably OloHeaderTool (code generation — see Tooling).
docs/Design docs, ADRs, build/testing guides, and agent rules — see Documentation.

Getting started

Supported platforms

  • Windows — Visual Studio 2026 (the default msvc preset) or Visual Studio 2022.
  • Linux — GCC 14+.
  • WSL2 — can compile every target and run OloServer, but not OloEditor: WSL2's software renderer only exposes OpenGL 4.5, and the editor requires a native OpenGL 4.6 GPU.

Requirements

  • CMake 4.2+ — required by CMakePresets.json (the msvc preset's Visual Studio 18 2026 generator only exists in 4.2+). The root CMakeLists.txt itself only requires 3.27, so a plain cmake -B build -G "Visual Studio 17 2022" still works without presets.
  • Python 3.10+ with the jinja2 package (needed to build glad2).
  • Vulkan SDK — for SPIR-V shader compilation.
  • Steamworks SDK — optional, and you must download it yourself: its licence forbids redistribution, so it is neither in this repo nor available through vcpkg. Needed only to work on Steam features (achievements, Steam Cloud, rich presence/overlay). Steam support is auto-detected: OLO_WITH_STEAM turns on when STEAMWORKS_SDK_ROOT points at a real SDK (a public/steam directory) and stays off otherwise — so without the SDK everything builds, runs and tests exactly as before, and with it installed you get the Steam path without passing a flag. The SDK itself is free — an ordinary Steam account and the Steamworks SDK Access Agreement, not the $100 Steam Direct publishing fee. Setup: docs/ops/build.md.

Clone

git clone https://github.com/drsnuggles8/OloEngineBase

Most third-party dependencies are resolved from the vcpkg.json manifest and built once into vcpkg's machine-global binary cache, so a second worktree restores them in seconds instead of recompiling (issue #773). VCPKG_ROOT must point at a bootstrapped vcpkg clone — see docs/ops/build.md; a configure without it fails with a pointer there. A handful of dependencies are still fetched at configure time via FetchContent/CPM into OloEngine/vendor/ (never edit that directory — a CMake reconfigure wipes it). CMake also creates the build/ directory with the generated solution files.

Building & running

CMake presets (CMakePresets.json):

PresetGenerator / build dirPurpose
msvcVisual Studio 18 2026 → build/Primary, default build dir
clangclNinja Multi-Config → build-clang/clang-cl warnings with the MSVC ABI
clangcl-asanNinja Multi-Config → build-clang/Adds AddressSanitizer
# Generate the Visual Studio solution
scripts\Win-GenerateProjectVS2026.bat        # or Win-GenerateProjectVS2022.bat

# Build a target
cmake --build build --target OloEditor       --config Debug --parallel
cmake --build build --target OloEngine-Tests --config Debug --parallel
cmake --build build --target OloRuntime      --config Debug --parallel
cmake --build build --target OloServer       --config Debug --parallel

# ClangCL (configure once, then build)
cmake --preset clangcl
cmake --build build-clang --target OloEngine-Tests --config Debug --parallel

Working directory matters. OloEditor, OloRuntime, and OloServer resolve assets, shaders, and Mono assemblies relative to OloEditor/. Always run them with cwd = OloEditor/. The VS Code tasks already do this.

VS Code users — predefined tasks in .vscode/tasks.json wrap all of the above: build-oloeditor-debug, run-oloeditor-debug, build-tests-debug, run-tests-debug, configure-clangcl, etc. Run them from the Command Palette (Ctrl+Shift+P → Tasks: Run Task).

CLion users — open the repo folder, let CLion configure the CMake project, then set the working directory of the OloEditor run configuration to the OloEditor/ folder.

The full per-OS build matrix (including Linux and WSL specifics) lives in docs/ops/build.md.

Features

Core engine

  • Entity-Component-System built on EnTT under a UUID-keyed Entity wrapper. The hottest update loops use EnTT owning groups for packed iteration.
  • Deterministic gameplay scheduler — per-tick systems declare their read/write data flow and the execution order is derived by topological sort, with optional worker-thread parallelism (audited per system) and a physics-shadow phase that steps the physics world alongside game-thread systems.
  • Multi-threaded asset system — async loading with hot-reload via filewatch; typed retrieval through AssetManager.
  • Dual scripting — C# via Mono (Windows) and Lua via Sol2 (all platforms), with generated bindings.
  • YAML serialization — scenes, entities, and prefabs. Trivial component (de)serialization, the ECS component tuple, save-game capture/restore lists, and add/remove handlers are all code-generated from the component definitions (see Tooling), so most new components round-trip with zero hand-written glue.
  • Save games — component-level snapshot/restore with a versioned binary archive format.
  • Custom memory management, threading, and task systems — custom allocators, RAII resources, a job/task scheduler, and async primitives.

Rendering

  • Modern OpenGL 4.6 with Direct State Access and SPIR-V shader compilation.
  • Stateless, layered, multi-threaded command queue (Molecular Matters' design) driven by a render-graph.
  • Physically-based rendering — metallic-roughness workflow, image-based lighting (IBL).
  • Deferred + forward paths with Hi-Z GPU occlusion culling and LOD.
  • Post-processing & advanced lighting — SSAO, SSR, SSGI, contact shadows, all shadow types (incl. PCSS), TAA, volumetric fog + god rays, screen-space decals, order-independent transparency, and FSR/CAS upscaling.
  • Unified 2D/3D pipeline — sprites and 3D meshes through one renderer.
  • Skeletal animation — Assimp-imported skeletons, animation graphs and state machines, additive layers, and humanoid retargeting; secondary motion via spring bones and cloth.
  • GPU-driven particles — compute-based particle simulation.
  • Real-time particle fluids — GPU Position-Based Fluids solver (compute spatial hash + density-constraint solve) with screen-space fluid rendering, deterministic CPU fallback, and two-way Jolt rigid-body coupling.

Physics

  • 3D — Jolt Physics for rigid bodies, joints, characters, and vehicles.
  • 2D — Box2D for 2D mechanics.
  • Custom collision layers for flexible filtering and detection.

Environmental & world systems

  • Procedural terrain generation with GPU-driven quadtree LOD, and optional virtual texturing — a feedback-driven page cache that moves the splat blend off the per-pixel path.
  • Water / FFT ocean rendering.
  • Weather & environment — wind, snow (with deformation), and precipitation systems.
  • Streaming volumes, light-probe volumes, fog volumes, and navigation-mesh bounds as scene-level authored data.

Gameplay, AI & narrative

  • AI — GOAP planner / action system and a perception system (sight / sound / awareness).
  • Navigation — Recast/Detour nav-mesh generation with nav agents.
  • Dialogue & quests, plus a localization system driving LocalizedTextComponent.
  • Cinematic sequencer — timeline-driven cutscenes.

Networking

  • Reliable UDP transport over GameNetworkingSockets, with encrypted transport (libsodium) and a protobuf wire format for snapshots / RPC. OloServer provides the headless dedicated-server target.

Audio & video

  • 3D spatial audio (miniaudio) with source/listener ECS components and a SoundGraph / MetaSounds-style audio-graph.
  • Video playback component.

UI system

  • ECS-based UI — Canvas, RectTransform, Panel, Text, Image, Button, Slider, Checkbox, Toggle, Progress Bar, Input Field, Dropdown, Scroll View, Grid Layout, WorldAnchor, and more.
  • Anchor layout — RectTransform-style anchoring, pivots, and auto-layout via grid containers; works in both 2D and 3D.
  • Editor integration — a "Create UI" menu, per-component property panels, and editor-time preview.
  • Full C# and Lua bindings for every UI component.

Editor & debugging

  • ImGui editor — scene hierarchy, content/asset browser with drag-and-drop, gizmos and transform tools, and real-time scene manipulation.
  • Play-in-editor — snapshot/restore around Play/Simulate.
  • Profiling — Tracy integration plus custom renderer profiler and GPU/CPU memory tracking.
  • Read-only MCP diagnostics server — inspect a live editor frame (screenshots, camera control, intermediate render targets, shader errors) from an external tool. See docs/guides/mcp-diagnostics-server.md.

Testing

The suite (OloEngine-Tests, GoogleTest) spans two independent axes:

  1. A renderer testing pyramid (L1–L11 plus plumbing / culling-LOD / shader-pipe / integration layers) covering pixels, GL state, and shader math — including screenshot-based visual-regression tests that gate GPU-equipped runs and skip cleanly when no OpenGL 4.6 context is present.
  2. A Functional / cross-subsystem axis ("Functional" tag) driving real Scene::OnUpdateRuntime ticks across Animation × Physics × Scripting × Networking × Audio × Asset × Nav × Save-game × Gameplay × AI seams.
# Run everything, or a single test
build\OloEngine\tests\Debug\OloEngine-Tests.exe
build\OloEngine\tests\Debug\OloEngine-Tests.exe --gtest_filter=SuiteName.TestName

The rationale, per-layer reference, classification rules, and anti-patterns live in docs/testing.md and docs/agent-rules/testing-architecture.md.

Continuous integration

Per-PR and per-push

These gate every change and run on GitHub-hosted runners.

WorkflowStatusCovers
WindowsWindowsMSVC build + full suite via CTest
SanitizersSanitizersASan (Windows/clang-cl), ASan+LSan / UBSan / TSan (Linux/Clang)
SonarCloud ScanSonarCloud ScanStatic analysis (see sonarqube-review-alignment.md)
Pre-commit checksPre-commit checksFormatting, YAML, test-catalogue classification

Scheduled

Cron times are UTC — GitHub's scheduler does not observe DST, so local times shift by an hour across the year. Every one of these is also workflow_dispatch-able from the Actions tab. Scheduled runs may start tens of minutes late under load.

WorkflowStatusCadence (UTC)Runs onCovers
GPU Conformance (AMD)GPU Conformance (AMD, self-hosted)nightly 00:47self-hosted Linux / AMD Navi 10The renderer's visual, golden and perf layers against a real GPU — they skip everywhere else. See self-hosted-gpu-runner.md
Cross-Vendor ConformanceCross-Vendor Conformancenightly 03:47GitHub-hosted WindowsL1/3/5/6/8 against Mesa llvmpipe (software)
Fuzz SmokeFuzz Smokenightly 04:13GitHub-hosted WindowsParser / deserializer fuzzing
Flaky repro (#281)Flaky repro (#281)nightly 07:00GitHub-hosted WindowsRepeat-runs a known-flaky test to hunt issue #281
Video (FFmpeg backend)Video (FFmpeg backend)weekly, Mon 06:00GitHub-hosted matrixOLO_VIDEO_FFMPEG=ON build + decode tests
reflection-status-checkreflection-status-checkmonthly, 1st 09:07GitHub-hosted LinuxReminder to re-check C++26 reflection toolchain support

Hardware-GPU vendor coverage is AMD-only. The goldens in assets/tests/golden/ are baselined on NVIDIA, but only ever from a developer machine — there is no NVIDIA or Intel CI. llvmpipe is a third software implementation, not a second vendor, so it catches spec-compliance drift rather than driver divergence.

Tooling

OloHeaderTool (tools/OloHeaderTool/) scans OloEngine/src/ and generates, at build time (via the GenerateBindings target):

  • C++ and C# scripting bindings from OLO_PROPERTY annotations.
  • The ECS AllComponents type list, the save-game capture/restore lists, the OnComponentAdded/OnComponentRemoved no-op handlers, and per-component scene serialize/deserialize blocks — all derived from the struct *Component definitions.

This collapses what used to be several hand-maintained, easy-to-forget touch-points into codegen: an all-trivial component now round-trips through scenes, save-games, and scripting with little or no hand-written glue. See CLAUDE.md for the exact contract when adding or changing a component.

Dependencies

Most dependencies come from the vcpkg.json manifest, pinned as a whole by its builtin-baseline registry commit and built into vcpkg's binary cache. The ones marked (in-tree) below are still fetched via FetchContent/CPM into OloEngine/vendor/, each for a specific reason documented in that file's header comment — briefly: sol2 and Lua are pinned to a tested pair, stb's port is ~2 years behind on an image decoder, ImGuizmo's port would drag in a second Dear ImGui, and the single-header / forked / generator-driven ones have no port at all. OpenUSD and FFmpeg have their own build paths (see CLAUDE.md).

Core — entt (ECS) · glm (math) · spdlog (logging) · yaml-cpp (serialization) · nlohmann/json (tools / IPC) · choc (utilities, in-tree) · atomic_queue (lock-free MPMC queue) · meshoptimizer

Rendering — glad (in-tree) · glfw · assimp · stb (in-tree) · zlib

Physics — Jolt Physics (3D) · box2d (2D)

Networking — GameNetworkingSockets · libsodium · protobuf

Navigation — recastnavigation

Audio — miniaudio

Scripting — sol2 (in-tree) · lua (in-tree) · Mono (C# runtime, manually integrated)

UI & editor — imgui (in-tree) · imguizmo (in-tree)

Development & profiling — tracy · googletest · filewatch (in-tree)

Code style & pre-commit hooks

We enforce a small set of formatting rules via pre-commit:

python -m pip install --user pre-commit
pre-commit install                 # enable the git hook (run in the repo root)
pre-commit run --all-files         # optional: apply across the whole repo

Included hooks: trailing-whitespace, end-of-file-fixer, and clang-format for C/C++ (uses the repo-root .clang-format). An .editorconfig mirrors the whitespace rules for editors. The hooks ignore vendor/, build/, and IDE metadata folders. A GitHub Action runs pre-commit on every push and PR.

Documentation

Everything under docs/ — start at docs/README.md for the full index. Highlights:

  • docs/ops/build.md — full Windows / Linux / WSL build matrix.
  • docs/testing.md — the canonical testing opinion doc.
  • docs/guides/ — subsystem how-tos (AI/GOAP, perception, terrain, UI, localization, cinematics, video, MCP diagnostics).
  • docs/adr/ — architecture decision records.
  • CLAUDE.md — contributor/agent playbook: build, test, and the component cross-binding contract.

Influences & references

  • Hazel — the foundation this engine started from (The Cherno's game-engine series).
  • Lumos — ideas for the OpenGL renderer and engine architecture.
  • Arc — ideas for the audio engine.
  • Molecular Matters Blog — the multi-threaded render command queue architecture.
Source 2 files
hooks/register.ts 114 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { ancestors, gateFor, guessRoot, newTestFileNotes, normPath, readGateMessage, relativeTo, resolvePath } from './gates'
4
5const FS_READ_LIMIT = 4 * 1024 * 1024
6
7// Files each loop (main, or a subagent by its id) has read this session, resolved and lower
8// case. Module state: a reload of this mod forgets them, which costs one re-read.
9const readByLoop = new Map<string, Set<string>>()
10// Checkout roots already found, by directory.
11const rootByDir = new Map<string, string | undefined>()
12
13function loopOf(agentId: unknown): string {
14  return typeof agentId === 'string' && agentId.length > 0 ? agentId : 'main'
15}
16
17/** Did this loop read THIS checkout's copy of the guide (not another worktree's)? */
18function hasRead(loop: string, guide: string): boolean {
19  return readByLoop.get(loop)?.has(normPath(guide)) ?? false
20}
21
22export const register: Register = on => {
23  on('tool.call', { tool: 'Read' }, async ($, e, next) => {
24    const ran = await next(e)
25    if (ran.deny === undefined && ran.isError !== true && typeof e.file_path === 'string') {
26      const loop = loopOf(e.agentId)
27      const set = readByLoop.get(loop) ?? new Set<string>()
28      set.add(normPath(e.file_path))
29      readByLoop.set(loop, set)
30    }
31    return ran
32  })
33
34  on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
35    const path = typeof e.file_path === 'string' ? resolvePath(e.file_path) : ''
36    const root = path.length === 0 ? undefined : await checkoutRoot($, path)
37    if (root === undefined) {
38      return next(e)
39    }
40    const exists = await $.fs.exists(path)
41    const incoming = e.tool === 'Write' ? e.content : e.tool === 'Edit' ? e.new_string : undefined
42    const needsText = /\.(?:h|hpp)$/i.test(path) && exists
43    const text = needsText ? await readSmall($, path) : undefined
44    const gate = gateFor(path, root, { isNew: !exists, text, incoming })
45
46    if (gate.kind === 'deny') {
47      return { deny: `olo-read-gates refused this edit: ${gate.reason}` }
48    }
49    if (gate.kind === 'requires-read') {
50      const guide = `${root}/${gate.doc}`
51      // A branch older than the guide has nothing to read: do not gate on a file that is not there.
52      if (!hasRead(loopOf(e.agentId), guide) && (await $.fs.exists(guide))) {
53        return { deny: readGateMessage(gate, path, root) }
54      }
55    }
56
57    const ran = await next(e)
58    if (ran.deny !== undefined || ran.isError === true || exists || e.tool !== 'Write') {
59      return ran
60    }
61    // A new test source was just written: is it built, and classified?
62    const rel = relativeTo(path, root) ?? ''
63    if (!rel.startsWith('oloengine/tests/') || !rel.endsWith('.cpp')) {
64      return ran
65    }
66    const notes = newTestFileNotes(
67      path,
68      root,
69      e.content,
70      await readSmall($, `${root}/OloEngine/tests/CMakeLists.txt`),
71      await readSmall($, `${root}/OloEngine/tests/scripts/test_catalogue.json`),
72    )
73    return notes.length === 0 ? ran : { ...ran, context: [...(ran.context ?? []), ...notes] }
74  })
75    // A gate that breaks fails OPEN: these are reminders of the repo's rules, and a bug in
76    // one must not block every edit in the session.
77    .catch(($, e, next) => next(e))
78}
79
80/**
81 * The checkout a file belongs to: the nearest directory holding a `.git` (a directory in the
82 * main checkout, a file in a worktree, either nested under `.claude/worktrees/`), falling back
83 * to the first top-level directory name when the file system cannot be asked.
84 */
85async function checkoutRoot($: EngineInterface, path: string): Promise<string | undefined> {
86  const dirs = ancestors(path)
87  for (const dir of dirs) {
88    const key = normPath(dir)
89    if (rootByDir.has(key)) {
90      return rootByDir.get(key)
91    }
92    try {
93      if (await $.fs.exists(`${dir}/.git`)) {
94        for (const below of dirs.slice(0, dirs.indexOf(dir) + 1)) {
95          rootByDir.set(normPath(below), dir)
96        }
97        return dir
98      }
99    } catch {
100      break
101    }
102  }
103  return guessRoot(path)
104}
105
106async function readSmall($: EngineInterface, path: string): Promise<string | undefined> {
107  try {
108    const stat = await $.fs.stat(path)
109    return stat.kind === 'file' && stat.size <= FS_READ_LIMIT ? await $.fs.read(path) : undefined
110  } catch {
111    return undefined
112  }
113}
114
hooks/gates.ts 194 lines
1// Pure rules for which files may be edited, and which guide must be read first.
2// CLAUDE.md states each of these as prose; this makes them hold.
3
4export type Gate =
5  | { kind: 'deny'; reason: string }
6  | { kind: 'requires-read'; doc: string; why: string }
7  | { kind: 'none' }
8
9const SHADER = /\.(?:glsl|vert|frag|comp|geom|tesc|tese|mesh|task|rgen|rchit|rahit|rmiss|rint|rcall)$/i
10const COMPONENT_DECL = /^\s*struct\s+(?:alignas\([^)]*\)\s+)?\w+Component\b(?!\s*;)/m
11
12export const DOCS = {
13  shaders: 'docs/agent-rules/glsl-shaders.md',
14  components: 'docs/agent-rules/component-serializer-codegen.md',
15  tests: 'docs/agent-rules/testing-architecture.md',
16} as const
17
18/**
19 * Forward slashes and `.` / `..` segments resolved, case kept: `<root>/OloEngine/src/../vendor/x`
20 * is the vendor file it names, whatever the spelling.
21 */
22export function resolvePath(p: string): string {
23  const slashed = p.replace(/\\/g, '/')
24  const drive = /^[A-Za-z]:/.exec(slashed)?.[0] ?? ''
25  const rest = slashed.slice(drive.length)
26  const isAbsolute = rest.startsWith('/')
27  const out: string[] = []
28  for (const part of rest.split('/')) {
29    if (part === '' || part === '.') {
30      continue
31    }
32    if (part === '..') {
33      if (out.length > 0 && out[out.length - 1] !== '..') {
34        out.pop()
35      } else if (!isAbsolute) {
36        out.push('..')
37      }
38      continue
39    }
40    out.push(part)
41  }
42  return `${drive}${isAbsolute ? '/' : ''}${out.join('/')}`
43}
44
45/** Resolved and lower case: how paths are compared here (Windows is case-insensitive). */
46export function normPath(p: string): string {
47  return resolvePath(p).toLowerCase()
48}
49
50/** Every directory above a file, nearest first: where to look for the checkout's `.git`. */
51export function ancestors(path: string): string[] {
52  const parts = resolvePath(path).split('/')
53  const out: string[] = []
54  for (let i = parts.length - 1; i > 0; i -= 1) {
55    const dir = parts.slice(0, i).join('/')
56    if (dir.length > 0 && !/^[A-Za-z]:$/.test(dir)) {
57      out.push(dir)
58    }
59  }
60  return out
61}
62
63/**
64 * A fallback root, for when the file system cannot be asked: the directory above the first
65 * top-level directory this repo has. The engine's own answer is the nearest `.git` (see
66 * register), which also places a nested checkout (`.claude/worktrees/<name>`) right.
67 */
68export function guessRoot(path: string): string | undefined {
69  const m = /^(.*?)\/(?:OloEngine|OloEditor|OloRuntime|OloServer|OloEngine-ScriptCore|docs|tools|cmake|scripts|build[\w-]*|\.github)\//.exec(resolvePath(path))
70  return m?.[1]
71}
72
73/** The path relative to `root`, lower case; undefined when it lies outside. */
74export function relativeTo(path: string, root: string): string | undefined {
75  const p = normPath(path)
76  const r = normPath(root).replace(/\/$/, '')
77  return p.startsWith(`${r}/`) ? p.slice(r.length + 1) : undefined
78}
79
80/**
81 * What an edit of `path` in the checkout at `root` needs. `text` is the file's current text
82 * where it matters (a header that may declare a component), `incoming` the text written.
83 */
84export function gateFor(path: string, root: string, opts: { isNew: boolean; text?: string; incoming?: string }): Gate {
85  const rel = relativeTo(path, root)
86  if (rel === undefined) {
87    return { kind: 'none' }
88  }
89
90  if (rel.startsWith('oloengine/vendor/') || rel.includes('/vcpkg_installed/') || rel.startsWith('vcpkg_installed/')) {
91    return {
92      kind: 'deny',
93      reason:
94        'This is a FetchContent download or a vcpkg port: a reconfigure wipes it, and the build never sees the edit for long. ' +
95        'Patch a vcpkg port through cmake/overlay-ports/, and a FetchContent dependency through its CMake fetch (CLAUDE.md, Architecture).',
96    }
97  }
98
99  if (/(?:^|\/)generated\//.test(rel) || /\.generated\.[a-z]+$/.test(rel)) {
100    return {
101      kind: 'deny',
102      reason:
103        'This file is generated by tools/OloHeaderTool (the GenerateBindings target) and is rewritten on the next build. ' +
104        'Change the source it reads (OloEngine/src) or the generator, build GenerateBindings, and re-stage the output (CLAUDE.md, OloHeaderTool). ' +
105        'A hand-broken generated file is also rewritten green by CI codegen, so it is no negative control either.',
106    }
107  }
108
109  if (/^docs\/test-catalogue\.[^/]*\.md$/.test(rel)) {
110    return {
111      kind: 'deny',
112      reason: 'docs/test-catalogue.*.md is generated and git-ignored: classify a test with `// OLO_TEST_LAYER:` or test_catalogue.json instead (CLAUDE.md, Definition of done).',
113    }
114  }
115
116  if (SHADER.test(rel)) {
117    return {
118      kind: 'requires-read',
119      doc: DOCS.shaders,
120      why: 'what makes a shader fail SPIR-V compile (CLAUDE.md names it a guide to read regardless of subsystem)',
121    }
122  }
123
124  if (rel.startsWith('oloengine/tests/') && rel.endsWith('.cpp') && opts.isNew) {
125    return {
126      kind: 'requires-read',
127      doc: DOCS.tests,
128      why: 'where a new test belongs; pick its classification before writing it (CLAUDE.md, Tests)',
129    }
130  }
131
132  const isHeader = /\.(?:h|hpp)$/.test(rel)
133  const declaresComponent = COMPONENT_DECL.test(opts.text ?? '') || COMPONENT_DECL.test(opts.incoming ?? '')
134  if (isHeader && (rel.startsWith('oloengine/src/') || rel.startsWith('oloeditor/src/')) && declaresComponent) {
135    return {
136      kind: 'requires-read',
137      doc: DOCS.components,
138      why: 'the cross-binding touch-points a component change needs (save-game Serialize + RegisterAll, Lua, the inspector and Add Component menu, a hand-written copy ctor / operator==); §0 is the table (CLAUDE.md, Definition of done 3)',
139    }
140  }
141
142  return { kind: 'none' }
143}
144
145export function readGateMessage(gate: { doc: string; why: string }, path: string, root: string): string {
146  return (
147    `olo-read-gates: read ${resolvePath(root)}/${gate.doc} before editing ${resolvePath(path).split('/').slice(-2).join('/')}. ` +
148    `It covers ${gate.why}. Read that checkout's copy (any part counts; read the part you need), then retry the edit.`
149  )
150}
151
152function escapeRegExp(s: string): string {
153  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
154}
155
156/**
157 * After a new test .cpp is written: is it built and classified? An entry counts only as a
158 * whole source entry (CMakeLists.txt lists paths relative to OloEngine/tests/) or a
159 * file_layer_map key (repo-relative): `OldNewThingTest.cpp` is no entry for `NewThingTest.cpp`.
160 */
161export function newTestFileNotes(path: string, root: string, content: string, cmakeLists: string | undefined, catalogue: string | undefined): string[] {
162  const rel = relativeTo(path, root) ?? ''
163  const relToTests = rel.startsWith('oloengine/tests/') ? rel.slice('oloengine/tests/'.length) : rel
164  const base = resolvePath(path).split('/').pop() ?? path
165  const notes: string[] = []
166
167  if (cmakeLists !== undefined) {
168    const entry = new RegExp(`(?:^|[\\s"(])${escapeRegExp(relToTests)}(?=[\\s")]|$)`, 'im')
169    if (!entry.test(cmakeLists.replace(/\\/g, '/'))) {
170      notes.push(
171        `${base} is not listed in OloEngine/tests/CMakeLists.txt (as \`${relToTests}\`), which names every test source: until it is, the file is never compiled and a --gtest_filter for it runs NOTHING (and the run still says PASSED).`,
172      )
173    }
174  }
175
176  const hasLayer = /^\s*\/\/\s*OLO_TEST_LAYER:\s*\S+/m.test(content)
177  let inCatalogue = false
178  if (catalogue !== undefined) {
179    try {
180      const map = (JSON.parse(catalogue) as { file_layer_map?: Record<string, unknown> }).file_layer_map ?? {}
181      inCatalogue = Object.keys(map).some(key => normPath(key) === rel)
182    } catch {
183      inCatalogue = false
184    }
185  }
186  if (!hasLayer && !inCatalogue) {
187    notes.push(`${base} has no \`// OLO_TEST_LAYER: <id>\` line near the top and no file_layer_map entry in test_catalogue.json: every test .cpp must be classified (CLAUDE.md, Definition of done 2).`)
188  }
189  if (/\bTEST(?:_F|_P)?\s*\(\s*\w+\s*,\s*DISABLED_/.test(content)) {
190    notes.push('A test here is DISABLED_: an evidence test that needs a GL 4.6 context skips cleanly without one instead (CLAUDE.md, Rendering changes must be visually verified).')
191  }
192  return notes.length === 0 ? [] : [`olo-read-gates: ${notes.join(' ')}`]
193}
194