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.
## 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** |
| `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: