Documentation Standards¶
Documentation is a deliverable, not an afterthought. This document defines the contract that every commit is held to.
The goal is specific: someone who has never met this codebase should be able to build an extension for it without reading its implementation. That is a higher bar than "the code is commented", and it drives everything below.
1. The four layers¶
| Layer | Lives in | Answers | Written |
|---|---|---|---|
| Architecture | Documentation/Architecture/ |
Why is the system shaped like this? | Before the code |
| Folder README | Code/<Module>/README.md |
What is this module and what does it own? | Before the code |
| API documentation | XML docs in source | How do I call this? | With the code |
| Guides | Documentation/Guides/ |
How do I accomplish a task? | With the feature |
A subsystem is not started until layers 1 and 2 exist, and not done until 3 and 4 do.
2. Folder README¶
Every module folder contains a README.md with exactly these sections:
# <Module Name>
**Purpose.** One paragraph. What this module is for.
**Responsibilities.** Bullet list. What it owns. Be specific about *state* ownership.
**Not responsible for.** Bullet list. The things people will wrongly assume it does,
with a pointer to what does own them.
**Dependencies.** Which modules this one depends on, and why each is needed.
**Public API.** The interfaces and events this module exposes, one line each.
**Events published.** What other modules can subscribe to.
**Persistence.** What this module saves, under which schema, at what version.
**Future improvements.** Known gaps and deliberate deferrals.
The "Not responsible for" section is not padding. Most integration bugs come from a reasonable wrong assumption about where a responsibility lives.
3. XML documentation¶
Required on every public type and member¶
No exceptions. A public member without a <summary> is a build warning, and warnings are
errors.
/// <summary>
/// Moves an item instance between two inventories, validating capacity on the destination
/// and ownership on the source. The move is atomic: on failure nothing is modified.
/// </summary>
/// <param name="item">The instance to move. Must currently be in <paramref name="from"/>.</param>
/// <param name="from">The source inventory.</param>
/// <param name="to">The destination inventory.</param>
/// <returns>
/// <see cref="TransferResult.Success"/>, or a failure describing why the move was refused.
/// The failure carries a player-facing message.
/// </returns>
/// <remarks>
/// Capacity is measured in <i>space</i>, not weight, and items with a negative
/// <see cref="ItemDefinition.Size"/> increase the destination's capacity. Removing such an
/// item is refused if it would leave the source over-encumbered — see
/// <c>Documentation/Legacy/03_INVENTORY_AND_ITEMS.md §1.2</c>.
/// </remarks>
/// <example>
/// <code>
/// var result = inventories.Transfer(pistol, character.Inventory, lockerContents);
/// if (!result.Succeeded)
/// character.Notify(result.Message, NotificationLevel.Failure);
/// </code>
/// </example>
public TransferResult Transfer(ItemInstance item, IInventory from, IInventory to)
| Tag | When |
|---|---|
<summary> |
Always |
<param> |
Every parameter, always |
<returns> |
Every non-void method |
<remarks> |
Whenever behaviour is surprising, or a rule comes from legacy tuning |
<example> |
Every member a framework consumer will call |
<exception> |
Every exception the caller is expected to handle |
<see cref="..."/> |
Instead of naming a type in prose — it gives the reader a link |
Write for the caller¶
A summary that restates the method name is worse than nothing, because it looks like documentation.
// ✗ Useless
/// <summary>Gets the inventory.</summary>
// ✓ Useful
/// <summary>
/// The character's carried inventory. Never null; a character always has an inventory,
/// even while dead. Contents are host-authoritative and replicate only to the owner.
/// </summary>
4. File headers¶
Every source file opens with a header block:
// -----------------------------------------------------------------------------
// InventoryTransferService.cs
//
// Moves item instances between inventories. This is the single mutation path for
// inventory contents -- nothing else may add or remove items.
//
// Owned by: Applejack.Inventories
// Depends on: Applejack.Core (events, logging), Applejack.Items (definitions)
// Legacy ref: libraries/sv_inventory.lua -- cider.inventory.update
// -----------------------------------------------------------------------------
Legacy ref is included wherever the file reimplements legacy behaviour, so a reader can
find the original specification in Documentation/Legacy/.
5. Inline comments¶
Explain why, never what¶
// ✗ Restates the code
// Loop through the items and add up their size.
foreach (var item in items) total += item.Size;
// ✓ Explains a decision
// Only positive sizes count toward space used; negative-size items (pockets) are
// capacity *grants* and are accumulated separately in MaximumSpace. Mixing the two
// here would let a pocket cancel out a rifle.
foreach (var item in items)
if (item.Size > 0) total += item.Size;
Cite legacy tuning¶
Any constant inherited from the original names its source. A future reader must be able to tell deliberate tuning from an arbitrary guess.
// Legacy: plugins/stamina/sv_init.lua. Drain is ~2.3x restore so sprinting has a real
// recovery cost. Below 50 HP both rates worsen proportionally -- this coupling IS the
// original's injury system. See Documentation/Legacy/08_NEEDS_AND_STATE.md §3.
[Property] public float StaminaDrain { get; set; } = 0.35f;
[Property] public float StaminaRestore { get; set; } = 0.15f;
Mark every deviation¶
Departing from the obvious approach, or from legacy behaviour, requires a comment naming the reason and linking the justification:
// We re-arrest on load rather than storing remaining time, matching the original's
// "you cannot log out of jail" rule -- but unlike the original we persist the ABSOLUTE
// release timestamp, because restarting the full sentence on reconnect was a bug.
// See Documentation/Legacy/06_CRIME_AND_POLICE.md §3.
Never¶
- Commented-out code. Delete it; git remembers.
// TODOwithout an owner and an issue link.- Comments that have gone stale. A wrong comment is worse than none — update or delete it in the same commit as the code.
- Decorative banners inside a method body.
6. Guides¶
Every public API gets a runnable example in Documentation/Guides/. A guide is
task-oriented ("add an item", "write a module"), not reference material, and it must:
- state what the reader will have at the end;
- show complete, compiling code, not fragments;
- explain the why at each step, not just the keystrokes;
- end with how to verify it worked.
If a guide's code stops compiling, that is a build failure, not a documentation nit. Guide snippets should be extracted from files that are actually compiled where practical.
7. Diagrams¶
Use Mermaid, in the markdown, not images — it diffs, it renders on GitHub, and it cannot go missing.
Required diagrams:
- A module dependency graph in
Architecture/00_OVERVIEW.md, kept current. - A sequence diagram for every cross-module flow more than three hops long.
- A state diagram for anything with more than three states (character physical state, interaction lifecycle, connection lifecycle).
8. Keeping it true¶
| Change | Documentation obligation |
|---|---|
| New public API | XML docs + a guide example |
| Changed behaviour | Update Architecture/, and the module README if responsibilities moved |
| New module | Folder README before the first line of code |
| Architectural decision | An ADR |
| New persisted field | Schema version bump + migration + Architecture/06_PERSISTENCE.md |
| Changed tuning value | Update the comment citing the legacy value, and say why it changed |
| Deleted feature | Update Legacy/01_FEATURE_INVENTORY.md disposition |
Documentation lands in the same commit as the code. Not the next one.
9. Tone¶
Write plainly. Short sentences. Active voice. Address the reader as "you".
Say what something does and what it costs. Do not sell. If an API is awkward, say so and say why it is awkward — that is more useful than pretending otherwise, and it tells the next person whether they are allowed to fix it.
Never write "simply", "just", "obviously", or "easy". If it were, the reader would not be reading.