Conventions
These are the rules a change has to follow to pass review and CI. This page is the
human-readable summary. The authoritative, machine-readable source — the
single source of truth that the project’s AI agents also follow — lives in the
repository under .agent/engine/ (conventions.md and RULES.md). When the two
ever disagree, those files win; this page is a digest, not a duplicate.
Math
- All math goes through
khora_core::math—Vec2/3/4,Mat3/4,Quaternion,Aabb,LinearRgba, and so on. Never use rawglam. If you need an operation the module doesn’t expose, extend the module rather than reaching for the dependency directly. - The convention is right-handed, column-major, Y-up. Document any non-trivial derivation with the formula or paper it comes from.
Logging
- Log through
log::{info, warn, error, debug, trace}— neverprintln!oreprintln!. Those bypass the log pipeline and the editor console.infofor lifecycle events,warnfor recoverable anomalies,errorfor unrecoverable errors before bubbling aResult,debug/tracefor gated hot-path diagnostics.
- One exception: the engine’s own log sink,
EditorLogCapture::log(inkhora-core/src/ui/editor/log_capture.rs), writes to stderr witheprintln!— alog::Logimplementation that calledlog::*would recurse forever. That is the only sanctionedeprintln!in the codebase.
Errors
- Never
unwrap()on a fallible GPU or I/O operation. One panic in render or asset code takes down the frame loop. UseResult, the?operator, ormap_errto add context. - Each subsystem owns its error enum (
LaneError,AssetError,PhysicsError, …), derived withthiserror, with variants that carry context (paths, IDs, expected state). Validate at boundaries (user input, file I/O, GPU); trust internal contracts.
unsafe
- Every
unsafeblock carries a// SAFETY:comment explaining why the invariant holds. No exceptions.
Components and the ECS
- Use
#[derive(Component)]on every ECS component. The macro generates the serializable mirror plus theFromconversions, and self-registers the component viainventory— you don’t wire it up by hand. - Declare a component’s domain with
#[component(domain = Physics)](or the relevant domain). - Use
#[component(skip)]for fields that must not serialize (GPU handles, runtime caches) and#[component(no_serializable)]for components with a manual mirror.
Agents
- An agent implements only
AgentandDefault— nothing else. Nostart/stop, no builders, no accessors; construction is viaDefault::default(). (Private free functions in the module file are fine.) - An agent owns exactly one
LaneKind, and its only jobs are: select a lane for the budget, negotiate that budget via GORNA, and dispatchLane::execute(). No per-frame state, no buffering outputs, no owning a flow. - Only use an agent for a subsystem that actually needs GORNA negotiation. For
everything else use a direct service (
AssetService,SerializationService, …).
Lanes and flows
- Lanes consume Views from the
LaneBus— they never query the World directly. The domainFlow(inkhora-data/src/flow/) is the only legitimate producer of those Views, and a flow is a read-only projector:select → project, never a mutation. - Never bypass the
Laneabstraction for hot-path work. - Adapt the HOW, never the WHAT. Automatic adaptation (DCC / GORNA / Flow / AGDF) may change representation — strategy, quality, memory layout — but must never change game semantics. Structural mutations are forbidden inside lanes.
Shaders
- Shaders are
.wgslfiles, never inline Rustconst/staticstrings. They live undercrates/khora-lanes/src/render_lane/shaders/—pipelines/for entry points,lib/for reusable modules — and are composed viaShaderRegistrywithnaga_oil#import. Write WGSL only (no GLSL or SPIR-V). - The four-bind-group budget. Every render lane uses exactly four bind groups:
0Frame (camera),1Object (per-draw model/normal matrix),2Material,3Lighting (the entire lighting domain). A new lighting feature adds bindings to group 3; it never adds a fifth group. Four groups is the universal wgpu baseline; bindings within a group are effectively unbounded.
Architecture boundaries
- Dependencies flow strictly downward:
khora-core→khora-data/khora-control→khora-lanes→khora-agents→khora-infra→khora-sdk. Never introduce a cycle. Abstract traits live inkhora-core; concrete backends (wgpu, Rapier, CPAL, Taffy, winit) live underkhora-infra. - Keep GPU resources behind abstract IDs (
TextureId,BufferId,PipelineId) — never expose raw wgpu handles in public APIs. - Concurrency goes through the DCC agent system — never
std::thread::spawndirectly (test code may, for isolation).
Before you push
- The workspace must build and test clean:
cargo test --workspaceis the primary check. cargo clippy --workspacemust be clean — CI runs it with-D warnings, so any warning fails the build.cargo fmtmust report no changes.
cargo xtask all runs format, clippy, build, test, and doc in one go — run it
before opening a pull request. See Workflow for the exact CI
gates.
Again: .agent/engine/conventions.md and .agent/engine/RULES.md in the repo are
the authoritative source. This page summarises them for humans — when in doubt,
read the source.