libakgl exports 157 functions and the only user-facing documentation was a FAQ in README.md whose examples did not compile: two unbalanced `PASS()` calls, 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. That is not a reader routing around typos. It is what happens to samples that nothing executes. This is akbasic's documentation harness with the interpreter taken out and the ability to link and run put in. The contract is a set of fence info strings, so an example is ordinary markdown that still highlights on the forge: ```c compiled with -fsyntax-only -Wall -Werror ```c wrap=NAME the same, wrapped in tests/docs_preludes/NAME.pre/.post ```c run=NAME linked against akgl and run headless ```c excerpt=PATH must still appear verbatim in PATH ```c screenshot=NAME also the source of docs/images/NAME.png ```json kind=KIND loaded through the real akgl_*_load_json ```output the exact stdout of the runnable block above it `run=` is why this links at all: -fsyntax-only proves a call typechecks, not that the startup order works or that an ATTEMPT block gives back what it took. `json kind=` exists because the asset formats are documented in prose and read by four loaders with nothing tying the two together -- util/assets/littleguy.json is already invalid against the loader it ships with. A fence with no info string is a hard error, and so is an unknown one. The failure mode this exists to prevent is passing because it quietly ran nothing, so an unannotated block is a missing decision rather than a default. Exit status is the number of failed examples; 2 for a usage error, which is a different thing and has to be distinguishable. Proven to fail on all ten of its failure modes -- untagged fence, non-compiling snippet, stale excerpt, orphan output block, unknown info string, run= output mismatch, invalid JSON, missing figure, a -Werror warning, and a dangling preload= -- because a check that has never failed has not been tested. `-Werror` here although AKGL_WERROR stays off for the library: that option is off so a new compiler's diagnostic cannot break a consumer's build, and a doc snippet is not a consumer. A sample that warns is a sample that teaches the warning. Registered as `docs_examples` and `docs_screenshots`. No docs-path filter in CI, deliberately: documentation goes stale because the code moved, not because somebody edited a chapter. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
256 lines
13 KiB
Markdown
256 lines
13 KiB
Markdown
# 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.
|
|
|
|
## 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:
|
|
|
|
| Setup | What it stages | What loads it |
|
|
|---|---|---|
|
|
| `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` |
|
|
|
|
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.
|