# Maintaining the manual

This file documents the harness that keeps `docs/` honest. It is not a chapter —
a reader learning libakgl never needs it — and it is checked by the same harness
it describes, so the file that publishes the convention is held to it.

**The harness checks that every listing is true. It cannot check that a chapter
teaches.** For that, the tutorials get a second test: the chapter is handed to a
subagent on a weaker model with the library, the headers and the art but *not*
the finished program, and that reader has to build and run the game. See
`AGENTS.md`, "Testing a Tutorial". Four read-only reviews of chapters 20 and 21
missed three defects the first build-and-run found in minutes.

## Documentation examples

**Every fenced block in `docs/*.md` is compiled, linked, run, or matched against
a source file by `ctest`.** The test is `docs_examples`; it runs in an ordinary
build and it fails the build.

It exists because the documentation this manual replaced had not been executed
since the day it was typed. In four hundred lines of `README.md`: two `PASS()`
calls with unbalanced parentheses, a `sprite->frameids = [0, 1, 2, 3];` that is
not C in any dialect, a stray `9` inside a bitmask expression, an
`int screenwidth = NULL`, and — in the first snippet a reader ever saw — the
exact `strncpy` call `AGENTS.md` forbids under **Copying Into Fixed-Width
Fields**. Two prose claims in the headers were false against `src/` as well.
That is not a reader routing around typos; it is what happens to samples nothing
runs.

Documentation goes stale because the **code** moved, not because somebody edited
a chapter. So this is an ordinary test case rather than something a docs-only CI
job runs behind a path filter — it fails when the library changes under the
chapter, which is precisely the case a path filter would miss.

**An untagged block is a failure**, not a default, and so is a tag this harness
does not know. That is deliberate: the way a harness like this dies is by
quietly matching nothing and passing, so an untagged block is a missing decision
rather than a free pass.

### The tags

| Info string | What happens |
|---|---|
| `c` | A whole translation unit. Compiled with `-fsyntax-only -std=gnu99 -Wall -Werror` against the real include path. It shows its own `#include` lines |
| `c wrap=NAME` | The same, with `tests/docs_preludes/NAME.pre` before it and `NAME.post` after |
| `c run=NAME` | Wrapped in `NAME`, **linked against `akgl` and run headless**. Must exit 0 and must not report an unhandled or ignored error |
| `c excerpt=PATH` | Must still appear in `PATH`, ignoring comments and whitespace. **Not compiled** |
| `json excerpt=PATH` | The same check against an asset file, for a fragment too large to show whole |
| `c screenshot=NAME` | Compiled as an `akglframe` body, and the source of `docs/images/NAME.png`. Optionally `size=WxH` |
| `json kind=KIND` | Written to a file and loaded through libakgl's own loader. `KIND` is `sprite`, `character`, `tilemap` or `properties` |
| `output` | The **exact stdout** of the runnable block above it, compared byte for byte |
| `sh` | Run in a sandbox. Must exit 0. A leading `$ ` is stripped |
| `sh norun` | Shown, not run. Destructive, networked, or it re-enters this suite |
| `cmake` | **Never executed.** Hand-maintained, by decision |
| `text` | **Never executed.** A block diagram, or a stack trace captured from a real run |
| `norun` | Shown, never executed, whatever it is |

These attributes combine with several of the above:

| Attribute | What it does |
|---|---|
| `setup=NAME` | Runs `tests/docs_setups/NAME.sh` in the sandbox first, with the repository root as its only argument. Valid on `c run=`, `json`, `sh` and `c screenshot=` |
| `size=WxH` | A figure's surface size. Only on `c screenshot=`; the default is 320x240 |
| `name=NAME` | Gives a `json` block a name a later block in the same chapter can preload |
| `preload=A,B` | Loads those named `json` blocks, in that order, before this one |

### Which one to reach for

In this order, and a step down the list is a decision that belongs in the prose:

- **`c run=akglapp` with an `output` block below it.** The strongest shape there
  is. `-fsyntax-only` proves a call typechecks; it does not prove the startup
  order works, that the sprite loaded, or that the `ATTEMPT` block gave back
  what it took. A `run` block proves all three, and the `output` block pins what
  the reader will actually see.
- **`c run=akglapp` alone**, where the example has no output. The harness still
  asserts a clean exit and that nothing raised.
- **`c wrap=akglbody`**, for a call sequence whose effect is not visible on
  stdout.
- **`c`**, for a translation unit the chapter wants shown whole — a header, a
  backend definition, a `main`.
- **`c excerpt=PATH`**, for a constant table, a `typedef` or a declaration.
  Compiling one of those would only redefine the type; the question worth asking
  is whether it still says what the header says.
- **`json kind=`**, for any asset file.
- **`norun`**, for a fragment or something that cannot be run here. Say why in
  the prose. **A rising `norun` count is a review finding** — it is the harness
  being talked out of its job.

### Preludes

`tests/docs_preludes/NAME.pre` and `NAME.post` bracket the block. They exist so
an example can be written the way a reader wants to read it while still
compiling — four lines of `PREPARE_ERROR` scaffolding repeated in twenty
chapters teaches the protocol twenty times and the call once.

| Prelude | What the block supplies |
|---|---|
| `akglfile` | File scope: declarations, a `movementlogicfunc`, a backend vtable. Includes only; there is no `.post` |
| `akglbody` | The inside of a function returning an error context, with `errctx` already prepared |
| `akglapp` | The same body, plus a `main` that brings libakgl up headless and runs it. This is what `run=` uses |
| `akglframe` | One frame of drawing, for a figure. Linked against `tools/docs_screenshot.c` |

All four begin with `tests/docs_preludes/docs_prelude.h`, which is the include
set — every public header, since the `headers` suite already proves each one is
self-contained. A block with no `wrap=` gets none of it and shows its own
includes, which is usually what a chapter wants a reader to see.

What does **not** belong in a prelude is anything that would let a wrong example
compile. A prelude declaring `akgl_game_init` itself would defeat the entire
check.

Compiler diagnostics point at the markdown: the harness emits a `#line`
directive before each body, so a broken example reports as
`docs/14-physics.md:112: error: too few arguments`, not as a line in a scratch
file nobody can open.

### Why `-Werror` here and not on the library

`AGENTS.md` keeps `AKGL_WERROR` **off** by default, and for a good reason:
libakgl is consumed with `add_subdirectory()` — `akbasic` does exactly that — so
a target-level `-Werror` turns every diagnostic a newer compiler invents into a
broken build for somebody else's project.

A documentation snippet is not a consumer. A sample that warns is a sample that
teaches the warning, and it is the maintainer of this repository who sees the
failure. So the snippets get `-Werror` and the library does not.

`gnu99` rather than `c99`, because `akerror.h` uses `PATH_MAX`, which
`<limits.h>` hides under `__STRICT_ANSI__`. A stricter dialect would fail on the
dependency rather than on the example.

### Asset formats

A `json kind=` block is written to a file and handed to `akgl_docs_checkjson`,
which brings libakgl up headless and calls the loader that really reads that
format — `akgl_sprite_load_json`, `akgl_character_load_json`,
`akgl_tilemap_load` or `akgl_registry_load_properties`.

This exists because the formats are described in prose here and parsed in
`src/`, with nothing tying the two together. That gap is not hypothetical:
`util/assets/littleguy.json` ships beside the loader it is invalid against.

The loaders really open the images and tilesets a block names, so a block that
references one needs a `setup=`. Adding a format is a row in the `KINDS` table
in `tools/docs_checkjson.c`, not a branch.

**Three of the four formats are not self-contained.** A character names sprites
that have to be in `AKGL_REGISTRY_SPRITE` already; a tilemap names characters,
and really opens its tileset image. There are two ways to satisfy that, and
which one to use depends on whether the dependency is something the reader
should see.

**Show it, with `name=` and `preload=`.** A `json kind=sprite name=herostand`
block earlier in a chapter and a `json kind=character preload=herostand` block
later in the same one are loaded into a single process, in that order. The
dependency ends up in front of the reader — which is right when the chapter is
teaching how the two files relate. Names are per-document, so one chapter cannot
reach into another and make the order documents are checked in load-bearing.

**Hide it, with `setup=`.** When the chapter is about one file and the others
would be noise, the fixtures go in a setup script and the `prepare` column of
the `KINDS` table in `tools/docs_checkjson.c` loads them:

| Setup | What it stages | What loads it |
|---|---|---|
| `setup=spritesheet` | `./spritesheet.png`, a 12x8 grid of 48x48 frames | the `sprite` row's `prepare` |
| `setup=character` | `./sprites/*.json` plus a sheet, named `hero standing left` and `hero walking left` | the `character` row's `prepare` |
| `setup=tilemap` | `./sprites/*.json`, `./characters/testcharacter.json`, and `assets/tileset.png` | the `tilemap` row's `prepare` |
| `setup=sidescroller` | every PNG and sprite file from `docs/tutorials/assets/sidescroller`, in `./` and `./sprites` | both rows' `prepare` |
| `setup=jrpg` | the same, from `docs/tutorials/assets/jrpg` | both rows' `prepare` |

The last two are how the tutorials show a reader a *real* asset file rather than a
generic one. A beginner following chapter 20 or 21 types what is on the page, so
what is on the page has to be a file that loads — the setup stages the tutorial's
own art so the block on the page is the block in the repository.

Those names are a contract between the setup script and the chapter using it.
Change one and change the other, in the same commit. `assets/tileset.png` is
deliberately not the RPG-Maker-named image `tests/assets/testmap.tmj` uses — see
the comment in `tests/docs_setups/tilemap.sh`.

Two things about map fixtures, both confirmed against `src/tilemap.c` rather
than against the prose:

- **Tilesets must be embedded.** `"source"` appears nowhere in the loader;
  `akgl_tilemap_load_tilesets_each` reads `columns`, `firstgid`, `tilecount` and
  `image` straight off each element of the map's own `tilesets` array. The
  external reference Tiled writes by default does not load.
- **Every object in an object group needs a `type` string**, plain rectangles
  included. A missing one fails the whole load.

### Figures are output, not assets

A chapter about drawing wants a picture of what the drawing calls draw, and the
way a picture goes wrong is that it stops being of the code beside it —
silently, because a hand-taken screenshot still looks like a screenshot long
after the call changed. So a figure is generated from the listing it
illustrates:

```sh norun
cmake --build build --target docs_screenshots
```

`tools/docs_screenshot.c` is a small second host: the dummy video driver, a
software renderer, one call to the block's `docs_frame()`, read the target back,
write a PNG. `tools/docs_screenshots.sh` compiles the listing against it.

Three rules, and `docs/images/README.md` carries the long version:

- **The PNGs are tracked.** A reader on the forge has no build tree. They are
  generated files tracked on purpose, so **do not hand-edit one** — change the
  listing and regenerate.
- **The build never regenerates them.** The target is only ever run
  deliberately.
- **`docs_examples` fails a tagged block with no image**, so a figure cannot be
  added to a chapter and then forgotten. Whether the image is *of that listing*
  is the separate `docs_screenshots` test's question: it re-renders every figure
  into a scratch directory and compares byte for byte, and it never repairs what
  it finds.

### When `docs_examples` fails

The message names the file and the line of the block's **opening fence**. For an
output mismatch it prints both sides through `cat -A`, because the errors it
catches are trailing spaces and missing newlines, which a plain diff renders
invisibly.

**Fix the documentation, not the expectation** — unless the library is what
changed, in which case fix the library first and the documentation second. Never
edit an `output` block to match output you have not looked at.

Run one document at a time while you work:

```sh norun
./tests/docs_examples.sh --root . \
    --cflags-file  build/docs_cflags.txt \
    --ldflags-file build/docs_ldflags.txt \
    --checkjson    build/akgl_docs_checkjson \
    docs/14-physics.md
```

Every path is made absolute by the script before it starts, because it `cd`s
into sandbox directories. A relative one used to fail every example and every
`setup=` — invisible under CTest, which passes absolute paths, and immediate for
anyone running one document by hand.

Omit a flag and the blocks that need it are **skipped and counted**, not
silently passed, so a partial invocation cannot look like a clean run.

**Exit status is the number of failed examples**, the house convention; `2` is a
usage or setup error, which is a different thing and has to be distinguishable.
A document named on the command line that cannot be read is `2` — never a
silent fall back to the default list, which is how a suite ends up passing
having checked nothing.

### Read the census line

The last two lines of a run report **what it executed, by kind**:

```text
ran: 41 c snippets, 6 linked programs, 6 output comparisons, 18 excerpts, 4 json blocks, 2 shell blocks, 3 figures
skipped: 5 norun, 2 cmake, 7 text, 0 c (no compiler flags given), 0 run (no linker flags given), 0 json (no validator given)
```

That count is part of the result, not decoration. A harness that passes because
a chapter stopped matching the extractor looks exactly like a harness that
passes because the documentation is correct, and the census is the only thing
that tells them apart. In `akbasic` it caught exactly that: a CMake generator
expression evaluated to an empty argument, the script read it as a filename, and
the suite passed having checked no documents at all.
