Imported from Zwaliebaba/Outpost.Commander (
AGENTS.md). Install upstream withnpx skills add Zwaliebaba/Outpost.Commander. Copyright stays with the author.
AGENTS.md — Engineering Rules
Operating instructions for every agent (and human) writing code in this repository. Read this before generating a single line.
This repository is a greenfield C++23 game and a hobby project with one developer: a Direct3D 12 game built on Windows with MSVC. This file is about how code is written here — naming, layout, build settings and the standing rules of the codebase. It is not the design: what the game is belongs in the design document.
The tree is young. This repository holds this file, the root configuration files, .gitignore, .github/, the design document, the ADRs, the art under Art/, the game's tuning and map data and its baked meshes under OutpostCommander/Assets/, the battle model under Tools/, and a solution with the game executable, a plain Win32 app packaged as MSIX (ADR-001), which opens a borderless full-screen D3D12 window on a menu, starts a match against the AI from it, draws the server's world from interpolated snapshots through the RTS camera, takes the player's selection and orders, runs the AI from its frame loop and the in-process server on a thread of its own, which both players reach over QUIC; the seven static libraries it links (§2); and unit-test DLLs for GameLogic and for GameApp. Nothing below is a target to migrate towards; it describes the code as it must be written from the first line. There is no legacy here and nothing is grandfathered, so a whole-tree run of any checker comes back clean — by conformance, not by exception.
Where these rules come from. They are carried over from two sibling repositories: Outpost.Warzone, where the formatter and linter settings were measured against roughly 223,000 lines, and Nomad-Commander. That lineage is why .clang-format and .clang-tidy are what they are, and it is why code can move between the trees without a rename or a reflow pass. What did not come across is the other trees' design, their decisions or their plan. A decision taken there binds nothing here.
What is authoritative, in order:
- This file — conformance: naming, style, build settings, and how to work here.
Design/ADR/— engineering decisions taken while building, one file per decision (§6), numbered fromADR-001in this repository and not continuing another's.- The surrounding code — for anything neither of the above covers, match the file you are editing.
The design document, GameDesign/Archive/OutpostCommander-MVP.md, as amended by GameDesign/Archive/OutpostCommander-Phase1.md and then by GameDesign/OutpostCommander-Phase2.md, sits alongside rather than above: it says what is built and this file says how. Where the design documents differ, the later phase's wins. A task that needs a design answer they do not give asks the owner, and gets the answer written down before the code is. The MVP and Phase 1 are done, and their documents are in GameDesign/Archive/: they still say what is built where Phase 2 does not amend them. The order the work was done in is the closed MVP plan, GameDesign/Archive/ImplementationPlan.md, and the closed Phase 1 plan, GameDesign/Archive/ImplementationPlan-Phase1.md: queues of tasks, delivered one PR per milestone (owner, 2026-09-30), with the owner decisions that gated them. A plan is a work queue, not an authority. Phase 2's design was accepted on 2026-10-03, and its plan, GameDesign/ImplementationPlan-Phase2.md, is the open one. Self-play, a side project that probes the rules and changes nothing a player sees, has a plan of its own, GameDesign/ImplementationPlan-SelfPlay.md, and its network a blueprint, Design/SelfPlayNetwork.md.
If a rule here conflicts with a habit from another codebase, this file wins. If you think a rule is wrong or your task cannot be done without deviating, say so in your report — never deviate silently.
1. Naming convention (normative — no exceptions)
| Kind | Convention | Example |
|---|---|---|
| Type (class, struct, enum, concept, alias) | PascalCase |
SwapChainTarget |
| Function, method | PascalCase |
PresentFrame() |
| Member variable | m_camelCase |
m_deviceRemoved |
| Static member (mutable) | sm_camelCase |
sm_activeDevice |
| Global | g_camelCase |
g_instance, g_frameCount |
| Parameter | _camelCase |
_fileName, _entityId |
| Local | camelCase |
shadedColor |
| Compile-time constant | UPPER_CASE |
WIDTH_PIXELS, CELL_PIXELS |
| Enumerator | PascalCase |
DeviceLost, OutOfVideoMemory |
| Macro | UPPER_CASE |
ENGINE_ASSERT |
| Namespace | PascalCase |
Engine |
| File | PascalCase.cpp / .h |
SwapChainTarget.cpp |
Note the split that catches people out: a constexpr is UPPER_CASE, an enumerator is PascalCase. They are both compile-time and they are spelled differently on purpose — an enumerator is a value of a type and reads as one at the use site (PageFault::OutOfVideoMemory), while a constant is a number with a name and is meant to look like one. .clang-tidy enforces both, and it is the single source of truth for the option values; this document states the rules in prose and does not repeat the settings, so there is nothing to drift.
The rules behind the table
R1 — The leading underscore on parameters is deliberate. It is legal C++: the reserved forms are _Uppercase, anything containing __, and _lowercase at global scope. A parameter is never at global scope, so _fileName is safe. Never introduce a reserved form — no _Impl, no __helper, no file-scope _cache (use g_cache in an anonymous namespace).
R2 — A type name carries no prefix or affix, and that includes abstract ones. An interface is Transport, not ITransport. A base class is not BaseTransport or AbstractTransport. PascalCase means the name and nothing else. This bans CFoo, SFoo, EFoo, IFoo, FooBase, FooAbstract, FooImpl and _t suffixes. Name the concept and let the concrete types say what they are:
Transport ← the concept
├── UdpTransport ← a socket-backed one
└── LoopbackTransport ← in-process, for tests
That tree is an illustration of the rule, not a description of anything. A base class for one derived class is ceremony: name the concept, and add the layer when a second thing needs it.
clang-tidy can require an absent prefix but cannot see a present suffix, so the repository checker carries the other half (§6).
R3 — Compile-time constants are UPPER_CASE. constexpr, inline constexpr and static constexpr members: WIDTH_PIXELS, TICKS_PER_SECOND, CELL_PIXELS. sm_ is reserved for mutable statics, which are rare and must document their thread-safety.
R4 — Acronyms capitalize as words: HlslSource, DxgiFactory, UdpTransport — never HLSLSource. Identifiers from an external SDK keep that SDK's spelling (ID3D12Device, DXGI_FORMAT, HRESULT, IDXGISwapChain4) and are never renamed to fit.
R5 — Template parameters are PascalCase: T, Fn, BlockBytes, Ts....
R6 — Units belong in names; types do not. durationTicks, speedUnitsPerSecond, radiusMeters, volumePercent are encouraged — a game measured in ticks, seconds, pixels and distances makes unit ambiguity a real defect class, and it is one the compiler cannot catch for you. Never encode the type: no iCount, pEntity, strName.
R7 — A file is named for its primary type, PascalCase, .h / .cpp only. .hpp, .cc and .inl are not used; template implementations live in the header. Exceptions, because MSBuild and the Visual Studio wizards spell them this way: pch.h, pch.cpp, framework.h, targetver.h, Resource.h.
R8 — m_ marks encapsulated state, not every field. A class with invariants prefixes private members m_. A public aggregate — a Desc config struct, a wire record, a POD handed to the renderer — uses plain camelCase fields so brace initialization reads naturally.
R9 — One namespace per layer, and the engine does not know the game. Reusable engine code gets its own namespace; game code gets another. Here the engine is Neuron and every game layer (GameProtocol, GameLogic, GameApp, Opponent) is Outpost. The split is a rule rather than a filing preference: if an engine type has to know a game concept by name in order to do its job, it is in the wrong layer. Test suites use namespace <Project>Tests.
R10 — No using namespace at file scope in a header. It leaks into every translation unit that includes it, and the failure it causes appears somewhere else. In a .cpp it is allowed for the unit-test framework and nothing else; otherwise qualify the name or write a local alias.
R11 — One spelling per family, and it is the SDK's. color, initialize, serialize, normalize, quantize, synchronize, behavior, neighbor, center, gray, canceled. Neither spelling is wrong English; the defect is a tree where a reader has to know which half they are in and a grep for one finds half the uses. D3D12_CLEAR_VALUE::Color settles which half wins. Prose is not checked — a design document may spell flavour and harbour; an identifier spells flavor and harbor. That holds for the design's own nouns too: its Defence gun and armour are DefenseGun and armor in code.
Worked example — this is the target style
// Engine/SceneTarget.h
#pragma once
#include <cstdint>
namespace Engine
{
// R3: constant → UPPER_CASE. R6: the unit is in the name.
inline constexpr std::uint32_t SCREEN_WIDTH_PIXELS = 1920;
inline constexpr std::uint32_t SCREEN_HEIGHT_PIXELS = 1080;
// Enumerator → PascalCase, unlike the constants above.
enum class TargetFault : std::uint8_t
{
DeviceRemoved,
BadFormat,
OutOfVideoMemory
};
/// The colour framebuffer the game draws into, and the depth buffer that goes with it.
/// R2: no prefix on the type. R8: private state carries m_.
class SceneTarget
{
public:
struct Desc // R8: aggregate → plain fields
{
std::uint32_t widthPixels; // R6: unit in the name
std::uint32_t heightPixels;
DXGI_FORMAT colorFormat; // R4: SDK spelling kept as-is
};
[[nodiscard]] static bool Create(ID3D12Device* _device, // R1: _ on parameters
const Desc& _desc,
SceneTarget& _outTarget) noexcept;
[[nodiscard]] std::uint32_t WidthPixels() const noexcept { return m_widthPixels; }
private:
ID3D12Resource* m_depthTarget = nullptr;
std::uint32_t m_widthPixels = 0;
bool m_deviceRemoved = false;
};
} // namespace Engine
Enforcement
| Rule | Enforced by |
|---|---|
| The naming table, R1, R3, R5, R8 | .clang-tidy, gated in CI over the whole tree |
| R2 affixes, R7 file names and project registration, R11 spellings, §2 flat directories, shader names and functional filters | Build/CheckProjectFiles.py, gated in CI |
| R4, R6, R9, R10 | Review. Check your own diff against the table before handing it back. |
2. Repository shape
The solution is OutpostCommander.slnx at the repository root. Its projects, and the edges between them, are fixed by ADR-002, which also gives the include-path table that enforces them:
| Project | Kind | Layer | Builds on |
|---|---|---|---|
NeuronCore |
static lib | engine, shared by client and server | — |
NeuronClient |
static lib | engine, client only | NeuronCore |
NeuronServer |
static lib | engine, server only | NeuronCore |
GameProtocol |
static lib | game, shared: commands, snapshots, IDs | NeuronCore |
Opponent |
static lib | game: the AI player, a client | GameProtocol |
GameLogic |
static lib | game: the authoritative server | NeuronServer, GameProtocol |
GameApp |
static lib | game: the client | NeuronClient, GameProtocol |
OutpostCommander |
Win32 exe, MSIX-packaged | shell | all of the above, but may include only NeuronCore, NeuronClient, GameProtocol, Opponent and GameApp |
GameLogicTests |
native unit-test DLL | test: drives GameLogic headlessly, and the AI against it | GameLogic, NeuronServer, GameProtocol, NeuronCore, Opponent |
GameAppTests |
native unit-test DLL | test: drives GameApp and NeuronClient without a GPU | GameApp, NeuronClient, GameProtocol, NeuronCore |
Every library has a master header named after it (NeuronCore.h, GameLogic.h, …) that includes the master headers of what it builds on, and its pch.h includes that header. Include another library through its master header or a header in its folder, and only if that library is on your project's include path. If a project is not in your row of the table, you cannot include its headers, and that is deliberate. The server moves to its own executable later (ADR-002), so a library references a package only where an ADR puts it (R14), and nothing at all depends on XAML or a WinRT API (ADR-001).
These are the standing constraints the layout satisfies, and any new project must satisfy them too.
Project directories are flat, with exactly two sanctioned subdirectories. C++ source — headers and .cpp alike — lives directly in its project's folder. There is no src/, no include/, and no other split of a project by file kind. This is not taste: .clang-tidy's HeaderFilterRegex matches headers exactly one level in, so a header in a subdirectory is silently unchecked — no findings, no warning, and nobody notices for months. The two exceptions are the shader pipeline:
<Lib>/Shader/holds the HLSL, hand-written, named for the shader and its stage:<Shader>VS.hlslfor a vertex shader and<Shader>PS.hlslfor a pixel shader.<Lib>/CompiledShader/holds what the compiler wrote: one header per.hlsl,<Shader>VS.hand<Shader>PS.h, each declaring a byte arrayg_<Shader>VS/g_<Shader>PS. It is build output — produced by anFXCompileitem in the.vcxprojon every build, listed in.gitignore, skipped by every checker, and never edited or committed. The.cppthat binds the pipeline state includes it and nothing else does.
Shaders are compiled into the executable. The bytecode reaches the GPU from those generated byte arrays, and nothing else: no .cso beside the .exe, no shader loaded from disk at runtime, no D3DCompile and no d3dcompiler_47.dll dependency. A shader change is a rebuild.
The edges run one way, and a layer never reaches sideways. Engine code is built on by game code and never the reverse (R9), and two libraries at the same level share what is below them rather than each other. An edge that only exists "for now" is an edge, and it is the one that will be impossible to remove later.
The project files are part of the source. Adding, removing or moving a file means editing the owning .vcxproj and its .filters. A file that compiles locally but is missing from the project fails only in CI — or worse, links a stale object nobody notices.
Filters are functional. A .filters file groups a project by what the code does — Rendering, Audio, Input, Shader — never by what kind of file it is. The Visual Studio defaults Source Files, Header Files and Resource Files are deleted when a project is created and never come back, and a .h sits in the same filter as its .cpp.
There are no vendored SDKs, apart from NeuronClient/d3dx12.h (ADR-007), and the package manager restores only what R14 lists. The build depends on the Windows SDK and the MSVC standard library, and on nothing else, apart from R14's exceptions. Each of those is restored through its project's packages.config into packages/: the two MSIX packaging tools in the executable (ADR-001), MsQuic in NeuronCore (ADR-004) and the PIX event runtime in NeuronClient (ADR-005).
Build and IDE output is never committed — x64/, ARM64/, .vs/, *.user, and anything a build step generates.
3. Build and verify
x64 and ARM64 are the only platforms (ADR-003). Every project and the solution have both, each with Debug and Release. Do not add Win32/x86, 32-bit ARM or ARM64EC, and do not write code that only works at 32 bits. Platform-specific code, such as an intrinsic, sits behind _M_X64 / _M_ARM64 with an implementation for each; code that builds on only one platform is a defect.
The compiler settings are the settings. Toolset v145 (Visual Studio 2026), /std:c++latest, /permissive-, /W4 with warnings as errors, /fp:precise, and /arch:AVX2 on x64 or /arch:armv8.0 on ARM64 (R16). There is no CMake. If a build error tempts you to change the toolset, lower the language standard, turn off /permissive- or silence a warning — stop and report instead.
Debug and Release are aligned by rule, not by luck. Every setting that is not about optimisation reads identically in both configurations: language standard, conformance, warning level, include directories, precompiled header, floating-point model, instruction set. The two differ in exactly four things — Optimization, _DEBUG vs NDEBUG, FunctionLevelLinking/IntrinsicFunctions, and the linker's folding and LTCG switches. (MSBuild spells those four through a few more properties — UseDebugLibraries, RuntimeLibrary as the debug or release CRT, LinkIncremental, WholeProgramOptimization, EnableCOMDATFolding, OptimizeReferences — and that list is the whole of what may differ.)
x64 and ARM64 are aligned the same way. The instruction set is the one setting that differs between them (R16); everything else reads identically on both.
That alignment matters more than it looks, because CI builds Debug|x64 only (§6). Release is compiled by whoever ships, and a Release that quietly lost an include directory or sat on an older language standard would not be discovered until then. A static check of the two configurations is what stands in for the build nobody runs.
Build through the solution, never a .vcxproj directly. Output paths and cross-project include directories are anchored on $(SolutionDir), and MSBuild defines SolutionDir only for a solution build. Building a project file directly resolves every one of those paths against the project folder instead of the repository root. It does not fail — that is the problem. Output lands in the wrong folder, so the next solution build links against whichever copy is staler, and every cross-project include path becomes a directory that does not exist. The breakage is latent: it bites the first time a file reaches across projects, which may be weeks after someone got into the habit. To build one project, use /t:<ProjectName> on the solution.
# Everything, from the repository root, naming the solution.
msbuild <Solution>.slnx /p:Configuration=Debug /p:Platform=x64 /m /v:minimal /nologo
# One project, still through the solution.
msbuild <Solution>.slnx /t:<ProjectName> /p:Configuration=Debug /p:Platform=x64 /m /nologo
# Release, before you claim anything about it.
msbuild <Solution>.slnx /p:Configuration=Release /p:Platform=x64 /m /v:minimal /nologo
# ARM64, when a change is platform-specific (ADR-003). CI does not build it.
msbuild <Solution>.slnx /p:Configuration=Debug /p:Platform=ARM64 /m /v:minimal /nologo
A project does not put its own directory on the include path. cl.exe already searches the directory of the including file first for a quoted include, so #include "FileSys.h" from a .cpp in the same folder resolves without help. Only the directories of other projects are listed, as $(SolutionDir)<Project>.
Run the tests, through vstest.console.exe, over every suite the build produced.
vstest reports "no tests found" as a pass. An empty suite is therefore worse than no suite: it is a green check mark over a library nobody exercised. A new test project starts with a placeholder SuiteSmoke for exactly this reason, and deletes it when its first real test lands, never before.
Run the checkers before you push. They are seconds of Python and they are what CI runs:
python Build\CheckFormat.py # clang-format, whole tree. --fix rewrites the offenders
python Build\CheckProjectFiles.py # build shape and settings, project registration, R2/R7/R11
python Build\RunClangTidy.py # needs a Developer PowerShell (INCLUDE must be set)
python Tools\BakeMeshes.py --check # the committed .nmf meshes are what their glTF sources bake to (ADR-018)
A green build says nothing about whether the game draws. For anything touching rendering, input, audio or presentation, launch the executable and look at it.
Report what you actually did. "Builds clean, not run" and "builds and runs" are different claims. Never imply the second when you only did the first, and say which configurations you built.
4. Layout and formatting
.clang-format is the authority for C++ layout: 2-space indent, 140 columns, Allman braces, pointer and reference bound left, includes never reordered. .editorconfig covers everything clang-format does not — CRLF, UTF-8, final newline, trailing whitespace, and the non-C++ formats — and repeats the two numbers an editor needs before the first save.
This tree is formatted, and CI keeps it that way. A whole-tree format check here is a no-op. Format what you write; if the check fires, run --fix and commit the result rather than arguing with it.
- Do not reformat what your task did not touch. The check being green tree-wide means a drive-by reformat produces pure churn and buries your actual change.
- Include order is load-bearing and grouped by hand, which is why
SortIncludesisNever:pch.h, then<windows.h>before any D3D12/DXGI/XAudio2 header, then the rest of the SDK, then project headers, then the standard library. A formatter reordering these behind a change's back is a correctness risk, not a style preference. - One header owns the Windows macro family, and nothing else defines any of it.
NOMINMAX,WIN32_LEAN_AND_MEAN,NOMCX,NOSERVICE,NOHELP,NODRAWTEXT,NOBITMAPare set in that one header, before<windows.h>, and the project files deliberately define none of them. Two owners of one macro is C4005, and/WXmakes that fatal —/Dspells a bare macro as1where a#definespells it as nothing, so the collision is guaranteed rather than possible. If you need<windows.h>, include that header; do not add the macros yourself. - Do not silence a diagnostic with
#pragma warning(disable: ...)to make a build pass. Fix the cause, or report it.
5. Rules for this codebase
R12 — Graphics is Direct3D 12. COM lifetimes are RAII from the first line — a raw AddRef/Release pair in new code is a defect, not a style. A COM pointer is a winrt::com_ptr, and an HRESULT is checked with winrt::check_hresult. Both come from C++/WinRT's <winrt/base.h>, which the Windows SDK ships in its cppwinrt folder. Do not use Microsoft::WRL::ComPtr or a hand-rolled ThrowIfFailed. check_hresult throws winrt::hresult_error, and which failures the renderer handles instead of throwing, such as a removed device, is decided in ADR-006: none is handled, and a lost device is fatal and reported. Using <winrt/base.h> for COM is not using WinRT: there is no Windows.* header, no .idl and no C++/WinRT package (ADR-001). How the renderer is shaped — frames in flight, presentation, resolution, scaling, window style — is recorded in ADR-006, not here; what it leaves open, such as passes, multisampling and text (gate G7), is decided with the task that needs it and recorded as an ADR (§6).
R14 — No third-party dependencies and no package manager. The Windows SDK and the MSVC standard library, and nothing else. If you believe something is unavoidable, propose it in your report with what it buys and what it costs — do not add it. This is a closed list, not a high bar.
The exceptions are these, each recorded in an ADR, and this table is the complete list:
| Package | Project | What for | ADR |
|---|---|---|---|
Microsoft.Windows.SDK.BuildTools, Microsoft.Windows.SDK.BuildTools.MSIX |
OutpostCommander |
MSIX packaging. These are build-time tools, and the game compiles and links nothing from them. | ADR-001 |
Microsoft.Native.Quic.MsQuic.Schannel |
NeuronCore |
QUIC, which carries a match's commands and snapshots, to the in-process server for now (ADR-060) | ADR-004 |
WinPixEventRuntime |
NeuronClient |
PIX event markers, compiled in for Debug only | ADR-005 |
d3dx12.h, vendored rather than a package |
NeuronClient |
D3D12 helper structs, such as barriers and descriptor handles, through DirectXHelper.h |
ADR-007 |
Only the project in a package's row references it. That project merges the package's import library into its own .lib (Lib AdditionalDependencies), and lists the package's DLL and licence as content. An executable that links it gets all three through the project reference, and no other project names a package path. A package's headers are included only from that project's .cpp files, never from a header that another project includes. No project includes XAML or a WinRT API. <winrt/base.h> is used for COM (R12), and it is SDK content, not a package. Any other package needs a new ADR.
It binds what the executable is built from, not what a development tool needs. Scripts under Build/ and Tools/ never ship and never link, so a baker that needs Pillow does not reopen this rule. Third-party content is a different question, and this rule does not cover it: art, fonts and sound are allowed, and none of it needs an approval, a licence text or a record of where it came from (owner, 2026-10-01).
For Direct3D that list means what the Windows SDK installs: d3d12.h, dxgi1_6.h, DirectXMath.h, winrt/base.h (winrt::com_ptr and winrt::check_hresult, which R12 asks for) and the fxc/dxc compilers that FXCompile drives. It excludes what a D3D12 sample reaches for by reflex, because each is NuGet or GitHub content and not SDK content: the DirectX Agility SDK, DirectX-Headers, DirectXTK12, DirectXTex, and the DirectX Shader Compiler as a redistributable. The one exception is d3dx12.h, vendored under ADR-007, whose helpers write the barriers and descriptions. The PIX event runtime is NuGet content too; ADR-005 is what lets it in.
R15 — Memory is plain C++. new/delete where it must be, RAII everywhere, standard containers by default. No pool, slab or free-list allocator without a decision recorded in Design/ADR/.
R16 — The floating-point model and instruction set are stated, not inherited. Every project compiles /fp:precise, and /arch:AVX2 on x64 and /arch:armv8.0 on ARM64 (ADR-003), stated explicitly in the project file rather than inherited from an MSVC default — a default is not a decision, and the symptom of losing one is two builds of the same code disagreeing about the same sum with no line to blame. Both settings are identical in Debug and Release.
What /arch:AVX2 costs is named rather than waved at. It sets an AVX2 floor — Intel Haswell (2013) and AMD Excavator (2015); an older CPU meets an illegal instruction, not a message. It does not, on its own, change float results. Since Visual Studio 2022, /fp:precise generates no contractions: MSVC does not fuse a*b+c into an FMA unless /fp:contract or /fp:fast is given, or #pragma fp_contract(on) is written (Microsoft's documentation of /fp and fp_contract; before 2022, /fp:precise did contract). So contraction stays off by rule: no project passes /fp:contract, and no source writes #pragma fp_contract(on). Turning it on changes float results, and is a decision recorded as an ADR. What contraction-off does not buy is agreement across platforms: the same float code can still give different results on x64 and ARM64 wherever it reaches the standard library's math functions or DirectXMath, which are implemented separately for each. armv8.0 is the ARM64 baseline, so it costs no hardware. Raising it is a decision with a cost to name, the same as AVX2.
If the game needs a deterministic core — a simulation that replays, lockstep networking, a result that must reproduce from a seed — that is a decision recorded as an ADR, and inside that core: no float where a fixed-point or integer quantity will do (hold a fraction as integer hundredths and say so in the name, R6), no iteration over an unordered container whose order reaches the outcome, no wall-clock time (a tick is the clock, and wall time maps to ticks at one seam), and randomness from a pinned PRNG with a recorded seed — never std::random_device, never a hash of an address. The game has one: ADR-009 records how these rules apply to Simulation, including where floats are allowed under its same-binary replay promise.
R17 — A string you do not write is const. /permissive- turns on /Zc:strictStrings: a literal is const char[N] and will not bind to char*. The fix is const on the signature, never a cast at the call site — a const_cast here is a lie about a literal that lives in a read-only section, and writing through it is a real crash rather than a theoretical one.
R18 and up are reserved. A design document does not only say what to build; some of what it says constrains how the code is shaped — which state a decision routine may read, what an emitted event has to carry with it, where tuning values live. Those are conformance rules with a design source, and they are written here as R18 onward when there is a design to cite, without renumbering anything above. Until then, do not invent one and do not import one from another tree: a rule with no source behind it is a rule nobody can settle an argument with.
6. Working rules
Stay in scope. Do what the task asks. Adjacent code that offends you is not part of the task — note it in your report and move on. Unrequested "while I was in there" changes are the main way a young tree acquires regressions it cannot bisect.
Record decisions as ADRs. An engineering decision — a file format, a wire protocol, a subsystem's shape, an exception to a rule here — goes in Design/ADR/ as one file per decision, numbered in order from ADR-001-<slug>.md, stating the context, the decision and what it forecloses, in the same commit as the change that implements it. An ADR is edited in place: a decision that changes is rewritten where it stands, so each ADR says what is decided now, not how it got there, and a decision that is dropped is deleted. Git has the history. A new ADR that overrules part of an older one rewrites that part to the current state, briefly, and names itself as the owner of the detail, so no ADR is left saying something that no longer holds and no status line says "superseded". An ADR overruled entirely is deleted, and what still holds of it moves to the one that overruled it. Numbers are never reused or shifted: a deleted ADR leaves a gap, and an ADR's decisions keep their numbers, because code comments and other ADRs cite them. Figures in an ADR are measured, not estimated — if you quote one, say how you measured it. A decision nobody wrote down gets re-litigated every few months by whoever forgot it.
Write the checkers early. Build/CheckFormat.py, Build/CheckProjectFiles.py and Build/RunClangTidy.py are what §1, §2 and §3 lean on, and all three gate in CI. CheckProjectFiles.py and RunClangTidy.py also have a --self-test, which CI runs, that shows each check still fires on a deliberately broken input.
What CI runs. .github/workflows/build.yml has four jobs: a Windows job that checks the build shape, builds Debug|x64 and runs the test suites; a Windows job beside it that runs clang-tidy, building only the shader headers it reads; a Linux job that checks formatting on a pinned clang-format; and a Linux job that checks the committed meshes are what their sources bake to (ADR-018). Every step that has something to run blocks; a step whose input does not exist yet is skipped, not faked. A step whose input can still be absent is guarded on it — the solution and the *Tests.vcxproj projects — and is skipped, not faked, while it is absent; the checker steps carry no guard, because the checkers are permanently in the tree. The guards are the only concession: nothing is continue-on-error, and a script that exists and fails still fails the build. Remove a guard once its input is permanently there, not before, and never add one to get past a red build.
CI does not build Release. The Windows build is the slow half of the pipeline and a second configuration roughly doubles it for a tree where the two differ only in optimisation. What stands in for it is the static alignment check on the two configurations (§3) — and, before a release, an actual Configuration=Release build by whoever is shipping. If you change something that could plausibly break only under optimisation, build Release yourself and say so.
Commits and PRs. Branch off main; small, focused commits with an imperative subject describing the change, not the process. One milestone per PR, in the order the plan gives, and one concern per commit. CI must be green. Never commit build output, .vs/ or .user files.
7. Before you hand work back
- Naming conforms to §1 —
_on parameters,m_on class state,UPPER_CASEconstants,PascalCaseenumerators, noI/C/Baseaffixes. - Only the lines the task required were changed; no reformatting, no drive-by fixes.
- New, removed or moved files are in the
.vcxprojand the.filtersof every project involved. - No project's
ConformanceMode,LanguageStandard,WarningLevelorTreatWarningAsErrorwas changed, and no warning was silenced with a pragma. - Debug and Release still agree on everything §3 says they must.
- No new third-party dependency (R14).
- The checkers pass — or, for one not yet written, the report says which and why.
- It builds Debug|x64, and every test suite runs and passes. If it is platform-specific, it also builds Debug|ARM64.
- If it touches rendering, input, audio or presentation: it was run, not just built.
-
Design/ADR/has a new file if the change was a decision. - Your report states plainly what you verified, what you assumed, and any rule here you had to bend.
