reset() now calls socket.clearSimId() so the next play() starts a fresh
simulation (CreateSimulation) instead of re-attaching to the old, reset
sim still alive within its TTL. Test asserts the post-reset play emits
CreateSimulation, not AttachSimulation.
Task: server-side-simulation review round 2, issue 001.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- manager: surface worker_thread errors/exit to clients and tear the sim
down (issue 001); cache last status/state and replay on attach so a
reconnecting client sees current state even when paused (issue 002)
- web socket: capped exponential reconnect backoff + bounded command queue
(issue 003); derive ws/wss URL from page origin (issue 004)
- runner: build replacement engine before freeing the old one (issue 005)
- remove dead VesselConfig message path across types/api/web (issue 006)
Tests added for each fix. Full monorepo suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fan-out and router roundtrip tests waited a fixed 180ms for the 20 Hz
loop to emit a State, which was flaky under CPU contention (full turbo
run). Poll for the State event with vi.waitFor instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single engine now lives on the server. Delete the simulation Web Worker,
the JS mock (mock-wasm), apps/web/public/wasm, and retire WorkerMessageDTO
and the wasm|mock SimulationEngine union from @ymir/types. Drop the
COOP/COEP headers and worker.format from vite config (were only for
in-browser WASM).
Task: task_08. Verified: web typecheck + unit suite green (83%), types
100%, api build clean, no dangling references. E2E against a live server
is validated separately (requires the running apps/api + web).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the local WASM Web Worker with the WebSocket client. Public store
API (play/pause/reset/loadScenario/applyEnvironment) is preserved; add
sendActuator + hasSession and a connection state (replacing the wasm/mock
engine badge). Update VesselPanel, AppShell, SimulationControls and their
tests; stub WebSocket in the jsdom test setup.
Task: task_07. Tests: full web suite green; store 95.8%, socket 93.9%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client transport to the server-side simulation: manages the WebSocket
lifecycle with auto-reconnect, (de)serializes ClientMessage/ServerMessage,
persists sim_id in localStorage, and re-attaches to the running sim on
reopen. Injectable WebSocket/storage for testing.
Task: task_06. Tests: 9 passing (FakeWebSocket), simulation-socket.ts
93.9% coverage; full web suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add @fastify/websocket (v8, Fastify 4) and a GET /ws endpoint. MessageRouter
validates inbound ClientMessages (TypeBox), dispatches to SimulationManager,
binds each connection to its sim_id on Create/Attach, and maps worker events
to ServerMessage frames. REST routes coexist unchanged.
Task: task_05. Tests: 31 passing (real Fastify + WebSocket roundtrip,
malformed-message handling, REST coexistence); branches 82.5%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owns every live simulation on the Fastify main thread: spawns one
worker_thread per sim (ADR-002), maps sim_id -> worker + connections,
fans events out to connected clients, enforces a max-sim limit, and
tears orphaned sims down after a configurable TTL (ADR-006). Make
SimulationRunner.dispose idempotent.
Task: task_04. Tests: 19 passing incl. a real worker_thread booting the
engine; coverage 93.6% stmts / 88% branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server-side simulation runtime: SimulationRunner owns one real WASM
engine and its 20 Hz tick loop, applying commands (loadScenario,
play/pause/reset, actuators, environment) and producing snapshots.
attachRunnerToPort bridges it to a worker_thread port; the thread
entrypoint lives in simulation-worker.entry.ts. Adds vitest to apps/api.
Task: task_03. Tests: 12 passing (real engine + MessageChannel bridge),
coverage 98.6% stmts / 89.5% branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat blue water plane with a Gerstner-wave ShaderMaterial.
Wave components are derived from the environment store's active wave
keyframe (Hs, Tp, dirNaut): wavelength from the deep-water dispersion
relation, amplitude anchored to Hs/2, direction from the nautical
bearing. The detail patch follows the camera (snapped to its grid) with
a flat far-sea plane behind for the horizon; phase animates off the
render clock so the surface moves regardless of sim play/pause.
Add a SceneErrorBoundary around the GLTF-loading area mesh and each
vessel so a missing/4xx .glb (assets are gitignored) renders nothing
instead of throwing up the tree and blanking the whole Canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vessel mesh's bow points along local -Z, but the heading was applied with no
offset, so a heading of 0 (East/+X) left the hull beam-on to its motion ("anda de
lado"). Apply a -90° yaw offset so the bow aligns with the travel direction, to
both the model and the onboard cameras (their offsets are in the mesh's -Z-forward
frame). Verified in-browser: surging East now moves bow-first.
Also start the free-fly camera from a closer 3/4 aerial that frames the vessel.
Note: the bay terrain/islands/bridge are placed on consistent axes but the vessel
at sim origin sits in open water far from the coast — that's the geographic
mesh-origin↔sim-origin alignment, still pending calibration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waves loaded into the timeline but no force model consumed them in the WASM
build, so vessels only translated (current → surge/sway/yaw). Wire the 6-DOF wave
excitation:
- core: WaveForces::setSpectrum() to swap the sea state without rebuilding (the
domain has no model-replace), keeping the RAO tables.
- core/src/wasm/WaveRaoData.h: embed the default vessel's excitation-force RAO
(vessel1.json waves, 6 DOF × 60 freq × 25 dir) so the WASM build has wave data
without file I/O.
- YmirBindings: add a WaveForces (RAO + calm spectrum) per vessel and a
setWaveConditions(Hs,Tp,dir,gamma,spectrum) that drives every vessel's spectrum.
- worker: parse the scenario wave keyframe and call setWaveConditions on env load.
- VesselPanel telemetry: show live φ (roll) / θ (pitch) / heave.
Verified in-browser: beam seas (Hs 6, Tp 14) → roll oscillates ±~1.3°, pitch ±~0.9°,
heave around equilibrium; head seas → near-zero roll (correct). Small angles because
the default hull is a 450,000 t VLCC. Core 257 tests + web 246 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 3D vessel only applied yaw, so even when the engine produced roll/pitch/heave
the ship rendered as a rigid block. Apply the full attitude: yaw about scene up,
then pitch about the beam axis and roll about the longitudinal axis in the
heading-aligned frame, plus heave bobbing on the waterline. Signs/axes documented
as calibratable. (Necessary half of the wave-motion fix; the wasm wave force is
wired separately.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The celso_furtado mesh origin is at the keel (world Y 0→46 = keel→masthead), so
placing it at the sea plane (Y=0) floated the whole ship above the water — and
near the shallow origin you saw the seabed around/under it. Sink the model by its
design waterline (~4.45 m, from vessel_config waterInteraction points) so the
waterline sits on the sea: hull partly submerged, superstructure above. Verified
in browser (Livre cam): vessel floats correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vessel appeared to float because AreaScene recentred the area group on the
combined bounding box, and ilhas.glb contains a stray vertex ~millions of units
out, so the box centre was garbage and shoved the terrain/water away from the
vessel. Verified against the uncompressed source: the meshes are Z-up with the
water surface at Z=0, modelled about the area's reference origin (= sim 0,0), so
the -90° X rotation already lands the waterline at Y=0 — no recentre needed.
- Remove the bbox recentre; trust the native origin (matches the sim frame).
- Enlarge the sea plane (200km) with a proper sea colour at Y=0 so the vessel
reads as floating on water and the bay sits in it.
Verified in browser (Livre cam): vessel on the sea, not in the air.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Livre" option to the onboard-camera selector. When active, the onboard
CameraRig is bypassed and FlyControls drives the camera: WASD to move, R/F up-down,
drag the mouse to look — go anywhere. On entry it positions above/behind the
selected vessel for an external overview, then hands control to the user.
viewStore CameraId gains 'Free'; CAMERAS map narrowed to the onboard ids with a
guard in CameraRig. CameraSelector + tests updated. 246 web tests green, tsc clean.
Verified in browser: external overview on select, keyboard fly confirmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Proa/Ponte/Popa/Boreste/Bombordo selector (the five celso_furtado onboard
cameras from vessel_config) shown over the 3D scene. Each camera uses its authored
local offset + yaw relative to the selected vessel; switching moves the view.
viewStore tracks the active camera; CameraSelector + tests added. 245 web tests
green, tsc clean. Verified in browser: switching cameras changes the viewpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New 3D background that swaps with the Leaflet map via a top-left Mapa/3D toggle —
only the background changes; all panels (top bar, sidebar, vessel panel, Play/Pause,
info) stay shared and live.
- Area3DView (react-three-fiber): loads the compressed Baía subset (batimetria +
ilhas + ponte, Z-up meshes rotated to Y-up and recentred on the sim origin) plus
a water plane and lighting; every vessel uses the celso_furtado mesh placed at
its live/draft pose.
- Camera uses the vessel's onboard "Front" camera (from vessel_config), following
the selected vessel; switching the selected vessel moves the view.
- geo3d util maps sim (x=East, y=North, ψ) → scene (X, -Z, yaw); viewStore holds
the mode; ViewToggle switches it. MapActions (Leaflet zoom) hidden in 3D.
Tests: geo3d + ViewToggle + viewStore; Area3DView excluded from coverage (WebGL,
e2e-only) like the Leaflet views. 243 web tests green, tsc clean. Verified in
browser: toggle both ways, onboard bridge camera, panels unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add three, @react-three/fiber, @react-three/drei (runtime) and gltf-transform /
gltfpack (dev) plus apps/web/scripts/build-3d-assets.sh, which compresses the
local Baía de Guanabara + celso_furtado source packs into web-ready GLBs
(meshopt geometry + WebP@1024 textures — no KTX2 transcoder needed under our
COOP/COEP). v1 subset = batimetria + ilhas + ponte + vessel (~21MB, down from
~640MB); topografia excluded (pathological per-tile textures). Generated GLBs are
gitignored — run the script once locally before using the 3D view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vessel DivIcon was rebuilt on every render. While a simulation runs the map
re-renders ~20 Hz, so Leaflet replaced each marker's DOM element every ~50 ms —
a click's mousedown and mouseup landed on different elements and the click was
swallowed, so the vessel panel only opened when stopped. Memoize the icon on the
rounded heading + selection so the element is stable; clicks (and switching
between vessels) now work at any point during the run. Verified in-browser: panel
opens mid-run with live telemetry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Context-driven UI pass (not a blind Figma copy):
- Top-bar "+" now opens a VesselPicker popover to add vessels (it was a dead
button; adding only worked from the sidebar). Removed the stale center
actionsDisabled gating that greyed "+" out before the first vessel existed.
- Scenario name is edited inline in the breadcrumb; dropped the duplicate name
field from the sidebar (still editable in the info panel).
- VesselList rows are a readable two-line card (name + remove, then x/y + heading)
instead of a cramped single line; adding moved to the top bar.
- Sidebar is collapsible so the map can go full-width (map-first focus).
New VesselPicker (+ tests); ScenarioForm/VesselList/Sidebar/AppShell updated.
236 web tests green, web+api tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the "Build simulation" step — the floating control is now just Play/Pause.
Pause freezes the simulation in place (worker keeps the body + elapsed time) and
Play resumes from there instead of restarting, so the instructor can pause, add an
environmental condition or another vessel, and continue. On resume the current
environment is (re)applied; if the vessel set changed, the engine is reseeded with
existing vessels at their live positions so the scene doesn't jump back to t=0.
- simulationStore: start/stop → play/pause; status 'ready' → 'paused'; add
applyEnvironment(); 'ready' worker msg no longer flips to a transient status.
- SimulationControl primitive reduced to Play/Pause; status strip shows Pausado.
- Remove dead legacy TelemetryPanel. Tests updated; web 237 green, web+api tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The legacy-row backfill spread `Record<string,number>` which tsc could not narrow
to InitialCondition, breaking `pnpm --filter @ymir/api build` (and the docker api
image). Construct each InitialCondition explicitly with defaults. Verified
`apps/api` tsc + full `docker compose build` + up (api/web healthy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ScenarioInfoPanel (Figma "Informações do cenário"): a right-docked Panel with
editable scenario name, area metadata (gravity, water density) and vessel count,
toggled by the top-bar info (ⓘ) button which reflects open state. New panel + tests;
240 web tests green. (Vessel AIS/RIPEAM tab deferred — no source data/spec yet.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the environment editors in the Tabs primitive per the Figma "Condições
ambientais" layout: the current/wind/wave keyframe editors live under the main
tab, and a Visibilidade tab adds Chuva/Neblina sliders (UI-only, not yet wired to
the physics engine — noted in the panel). Test added; 236 web tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add react-router-dom with two routes: "/" → MyScenariosPage (scenario grid with
search, create, open, delete against the scenarios API) and "/editor" → AppShell.
"Sair do cenário" returns to the list. New: use-scenarios hook, ScenarioCard,
MyScenariosPage (+ tests, 235 web tests green).
API: backfill legacy initialConditions rows missing instanceId so GET /scenarios
serializes under the updated schema.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
InitialCondition.vesselId held the per-scenario instance id, losing which vessel
TYPE each body was — so a saved scenario could not reload the right vessel config.
Add instanceId (engine body id) and keep vesselId as the type id; emit both from
the scenario store. (Resolves the flagged DTO ambiguity.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AppShell test stubbed fetch to resolve `{}`, but the mounted sidebar's
useAreas/useVessels hooks expect arrays and called `.map` on it asynchronously
after teardown — surfacing as vitest "unhandled errors" and a non-deterministic
exit 1 despite all assertions passing. Resolve list endpoints as `[]`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ScenarioForm, VesselList and EnvironmentConditionPanel now read colours, spacing,
radii and fonts from the design tokens instead of hard-coded literals; VesselList
uses the Button primitive and mono numerics. Behaviour, stores and test selectors
(aria-labels, button names) unchanged — 229 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace VesselPanel's bespoke inline layout with the shared primitives:
Panel (eyebrow "Embarcação Ownship" + close), Tabs (Geral/Controles), Card
sections, Slider and Field. AzimuthDial kept as a custom control. Behaviour and
store wiring unchanged — actuator commands still post via the worker keyed by
instanceId, live u/v/r telemetry preserved.
Panel forwards an overriding data-testid so the existing e2e selector
("vessel-panel") keeps working. Tests updated for the tab role.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the Figma "[INSTRUTOR]" port plus hardening found during testing.
Design system
- theme/tokens.ts + global.css: Figma palette, type scale, spacing, radii,
shadows; injected as CSS vars at boot.
- ui/: 11 themed primitives (Button, IconButton, TopBar, Panel, Tabs, Card,
Slider, Field, Modal/AlertDialog, SimulationControl, MapActions), each tested.
App shell (Layout do instrutor)
- features/shell/AppShell.tsx: TopBar (logo + breadcrumb, gated center actions,
Sair/Salvar), full-bleed map, bottom-center Build/Play/Stop pill, bottom-right
zoom, no-ownship AlertDialog. Replaces ScenarioCreatorPage.
- stores/mapStore.ts exposes the Leaflet instance so the zoom pill can drive it;
AreaMapView registers it and drops Leaflet's default zoom control.
Physics truth
- Worker reports which engine loaded (real WASM vs JS mock); surfaced as a
status badge/warning so the instructor never mistakes the mock for real
dynamics. SimulationControls is now a status-only strip (actions moved to the
pill).
Tests
- 229 passing, coverage gate green. New: tokens, all 11 primitives, AppShell,
VesselPanel, vesselPanelStore. Fixed GeralTab to read draft by instanceId.
Docs
- docs/specs/features/instructor-ui/: extracted screen specs, reference maps,
phased PLAN.md, and DYNAMICS-GAP.md (MATLAB↔Ymir physics parity).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Environmental Conditions feature:
- C++ EnvironmentTimeline: time-series current/wind data, JSON serialization
- WASM bindings: loadEnvironment(), World integration
- TypeScript types: CurrentSeries, WindSeries, EnvironmentPayload
- Zustand environmentStore with React panel (EnvironmentConditionPanel)
- Mock WASM current drift physics: nautical→body-frame velocity applied in step()
Bug fixes:
- Vessel instanceId: unique auto-increment ID per scenario instance prevents duplicate
vesselId crash when adding two vessels of same type
- vesselPanelStore: separate vesselTypeId field so config fetch uses correct type ID
(not instanceId) after the instanceId/vesselId split
- VesselMarker stable eventHandlers: useMemo + useRef pattern prevents react-leaflet
from re-registering Leaflet listeners every 50ms tick, fixing panel clicks during
active simulation
Tests:
- Playwright regression suite (e2e/regression.spec.ts): 8 tests covering panel open
during simulation, multi-click consistency, two VLCC no error, dual marker render,
individual panel open, remove-one behavior, current drift, no-drift baseline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CurrentForces::computeObokata was computing water velocity at hull sections
as (-vc_x, -(vc_y - r*xi)) instead of the correct (-vc_x, -vc_y - r*xi).
The sign error on the yaw term caused sections at bow/stern to generate
anti-damping yaw moments (positive feedback), producing exponential state
divergence when thruster + rudder were at maximum.
Fix: water_y = -vc_y - r*xi, matching MATLAB's relativeVelSection convention
(water_vel = fluid_vel - vessel_vel_at_section = -vesselVel - r*xi).
Also raise RK45 limits in YmirBindings (reltol=1e-4, abstol=1e-6,
maxStep=0.005, maxSteps=100000) to handle the transient at startup.
fix(web): correct vessel heading arrow direction on map
VesselMarker was applying psi_deg directly as CSS rotation degrees.
CSS rotate(0deg) = arrow North, but psi=0 = East in the physics world frame.
Fix: cssRotation = (90 - headingDeg + 360) % 360.
feat(web): add VesselPanel with live actuator controls
Vessel panel opens on map click, shows vessel info and live state,
exposes thruster power/azimuth and rudder angle sliders during simulation.
Includes vesselPanelStore for actuator state management and e2e tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
addVessel() after step() is undefined behaviour in the C++ engine — CVODE
integrator is already initialised for existing bodies. Calling reset() does
not clear the body registry, so addVessel() throws a C++ exception.
Fix: store SimulationClass reference from the Module, call .delete() on the
old instance (required by Emscripten Embind heap management), then construct
a fresh YmirSimulation and add vessels to the clean state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
require-corp blocked OpenStreetMap tile loading with
ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep.
credentialless still enables SharedArrayBuffer for WASM but allows
public cross-origin resources (OSM tiles) to load without credentials.
test(web): add simulation E2E spec covering full instructor flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
react-leaflet v5 requires React 19 — now fully compatible without
peer dependency warnings. No API changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
react-leaflet v5 requires React 19 — caused "render2 is not a function"
runtime crash and white screen. Downgraded to v4.2.1.
fix(docker): separate DB volume mount from API dist directory
Volume was mounted at /app/apps/api, overwriting compiled dist/ with stale
files from previous container runs. Changed mount to /app/data for DB only.
fix(api): copy area.json and vessel1.json into API Docker image
Seed files could not resolve JSON data at container startup.
Added COPY in Dockerfile and corrected path depth from 5 to 4 levels.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the Area entity: geographic origin (lat/lng = simulation 0,0),
GeoJSON polygon boundary, and environmental constants (gravity, water/air
density, magnetic correction). A simulation cannot run outside its area.
- areas table with origin, polygon (GeoJSON), and environment constants
- Auto-seed from area.json on startup (Baía de Guanabara)
- GET/POST/DELETE /areas endpoints (route → service → repository)
- scenarios.area_id FK — nullable for existing rows, idempotent ALTER migration
- AreaDTO + CreateAreaDTO in @ymir/types; ScenarioDTO gains optional areaId
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 6 supplementary SQLite tables (physical_props, current, connection_points,
rudders, thrusters, waves) to hold all hydrodynamic data for the VLCC
VLCC_L350_B63_T230 (L=350m, B=63m, T=23m).
- Auto-seed at startup: reads vessel1.json into all tables when DB is empty
- GET /vessels/:id/config endpoint returns full physics config for simulation
- Refactored vessels route to use service + repository layer pattern
- Expanded @ymir/types with VesselConfigDTO and all sub-component schemas
- Simulation worker fetches vessel config on init, passes to WASM loadConfig()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add dist/, *.profraw, .wasm-stage/ to .gitignore
- Remove apps/api/dist/, apps/web/dist/, packages/types/dist/ from git
(generated on build, should not be versioned)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When ymir.js is not built (Emscripten not installed), the worker falls back
to a pure-JS MockYmirSimulation that mirrors the Embind API. Vessel moves
with constant u=0.5 m/s and r=0.01 rad/s so the telemetry panel shows
live updating values without requiring the C++ build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove SUNDIALS from C++ Dockerfile (replaced by RK45 in T03)
- Fix CMake source path to core/ after T05 reorganization
- Add docker/api/Dockerfile: multi-stage Node 20 build for Fastify API
- Add docker/web/Dockerfile: Vite dev server with WASM volume mount
- Rewrite docker-compose.yml: core (C++ ci build), api, web services
- Add server.host=0.0.0.0 to vite.config.ts for Docker networking
Run everything: docker compose up --build
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move files from flat core/naval/adapters layout into libs/{common,physics,simulation,vessel,world,persistence} with updated namespaces, CMakeLists, tests, README, AGENTS.md, and Doxyfile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>