Add README, AGENTS.md, CLAUDE.md, and .gitignore. Establishes project identity (general-purpose physics engine, naval domain first), Ports and Adapters architecture rules, Conventional Commits, and Semantic Versioning as project standards. Legacy reference docs moved to .docs/ and gitignored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.0 KiB
Ymir — Agent Guidelines
Ground rules for AI agents (Claude, Copilot, etc.) working on this codebase.
Project context
Ymir is a general-purpose physics engine written from scratch in C++17. It is designed to simulate the dynamics of physical bodies under arbitrary force systems, and to be extended to any physical domain.
Current scope: naval domain. The first implementation covers 6-DOF hydrodynamics for ships and floating platforms. All architecture decisions must remain domain-agnostic — the naval module is one application of the engine, not the engine itself.
There is a reference implementation in .docs/ (legacy naval project). Use it to understand domain behavior and physics intent — do not copy its code structure or repeat its known problems.
Architecture rules
Ports and Adapters
core/must have zero knowledge of I/O, file formats, or rendering- All external dependencies (JSON, CSV, SUNDIALS, CGAL) live in adapters, not core
- The C API in
include/is the only public surface — everything else is internal
C API boundary
- No STL types cross the DLL boundary (
std::vector,std::string, etc.) - No C++ exceptions cross the DLL boundary — functions return error codes
- All structs at the boundary must be plain C (
sizeofmust be deterministic) - Calling convention:
extern "C", no__cdeclattribute needed (useCallingConvention.Cdeclon .NET side)
Physics core
- No heap allocation inside the integration loop (pre-allocate in constructors)
- No global mutable state — every simulation is an isolated object
- Force modules: pure functions of state — receive
const VesselState&, returnForces - CVODE memory owned by
BodyDynamics, cleaned up in destructor
Coding standards
- C++17 — use
std::optional, structured bindings,if constexprwhere appropriate - SOLID — single responsibility per class, no god objects
- No raw owning pointers — use
std::unique_ptr/std::shared_ptr - No
using namespace stdin headers - Doxygen on every public symbol (
/** */style) - No magic numbers — named constants or
constexpr
What to avoid (lessons from legacy)
| Problem | Rule |
|---|---|
Global state in wave engine (g_waveData, g_components) |
All state owned by instance objects |
rand() without seed — deterministic "random" phases |
Use std::mt19937 with explicit seed; expose seed as parameter |
wave_engine_prune corrupting indices on 2nd call |
Rebuild index after any mutation of the component list |
| Inconsistent angle normalization across modules | One AngleUtils namespace, used everywhere |
| Off-diagonal hydrostatic stiffness silently ignored | Either implement fully or document explicitly in code |
| Small-angle approximation for roll/pitch undocumented | Document assumptions with a // ASSUMPTION: comment |
| Mixing coordinate origins in restoring force calc | Each formula must name its reference frame in a comment |
| CSV output coupled to physics loop | OutputWriter is an adapter — physics loop calls an observer interface |
Testing
- Every force module has a unit test before it is integrated into
RHS - Unit tests use analytical solutions or known limits (e.g., zero current → zero current force)
- Integration tests run a full simulation and assert on conserved quantities or known maneuver behavior
- Test files mirror source structure:
tests/core/forces/TestCurrentForces.cpp
Documentation
- Architecture decisions go in
docs/adr/as ADR files (Architecture Decision Records) - Public API docs generated by Doxygen from source
- No inline
// TODOwithout a linked issue or a// REASON:explanation
Versioning
Ymir follows Semantic Versioning (semver.org):
MAJOR.MINOR.PATCH
| Bump | When |
|---|---|
MAJOR |
Breaking change to the public C API or physics model contract |
MINOR |
New capability added in a backward-compatible way |
PATCH |
Bug fix, doc update, internal refactor — no API change |
Version is declared in CMakeLists.txt (project(Ymir VERSION x.y.z)) and exposed in the C API header.
Commit discipline
Ymir follows Conventional Commits (conventionalcommits.org):
<type>(<scope>): <short description>
[optional body]
[optional footer: BREAKING CHANGE: ...]
Types
| Type | Use |
|---|---|
feat |
New feature (bumps MINOR) |
fix |
Bug fix (bumps PATCH) |
docs |
Documentation only |
test |
Adding or fixing tests |
refactor |
Code change that is neither fix nor feat |
perf |
Performance improvement |
build |
Build system, CMake, dependencies |
ci |
CI/CD pipeline changes |
chore |
Maintenance — no production code change |
Scopes (examples)
| Scope | What it covers |
|---|---|
core |
Engine-agnostic physics core |
naval |
Naval-domain force modules |
api |
Public C API (include/) |
adapters |
I/O adapters (JSON, CSV) |
tests |
Test infrastructure |
build |
CMake and toolchain |
Rules
- One logical change per commit
- Breaking C API or ABI changes require
BREAKING CHANGE:footer - No commented-out code in commits
- Tests must pass before committing to
main
Examples
feat(naval): add current force model for cross-flow drag
fix(core): correct inverted sign in restoring force heave term
refactor(api): replace raw pointer with unique_ptr in BodyDynamics
BREAKING CHANGE: realtime_create() now returns a typed handle, not void*