The agent instructions now say where new work goes -- an issue on the forge, carrying status::grooming until its scope is settled -- and that TODO.md is the record: why the handler ladder is a major-version change, why a copied akerr_ErrorContext is a trap, why validating more inputs lowers branch coverage, and why the mutation score is a floor. The instruction that matters most here is about consumers. Three defects in this library -- IGNORE() leaking a context, the un-namespaced coverage target, and the missing akerrorConfigVersion.cmake -- were written down in libakstdlib's notes and never here, so each was worked around once per consumer and nobody saw the pattern. Filing upstream should cost a consumer one issue instead of one workaround. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
8.1 KiB
Repository Guidelines
Project Structure & Module Organization
This is a small C library built with CMake. Core implementation lives in
src/error.c. src/lock.h is private: it selects the threading backend
(pthread or none) and is not installed. The public header is generated at build
time from include/akerror.tmpl.h by scripts/generrno.sh, which also
generates src/errno.c under the build directory and stamps in whether the
build is thread safe. CMake package and pkg-config templates are in cmake/ and
akerror.pc.in. Tests are one-file C programs in tests/; shared test helpers
live beside them, such as tests/err_capture.h and tests/err_threads.h.
Prose documentation lives in docs/, one file per topic — architecture.md,
usage.md, status-codes.md, uncaught-errors.md, exit-status.md,
thread-safety.md, building.md. README.md is deliberately short: summary,
installation, quickstart, and an index into docs/. Add new documentation to
the docs/ file that owns the topic and link it from the README's index rather
than growing the README back.
Build, Test, and Development Commands
Use an out-of-tree build:
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
cmake -S . -B build configures the build and generates akerror.h.
cmake --build build compiles libakerror and test executables. ctest
runs the registered unit tests. To emit CI-style results, use:
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
The suite includes thread tests. The run that proves there is no data race under them is ThreadSanitizer:
scripts/thread_test.sh
which configures build/tsan with -DAKERR_SANITIZE=thread, builds the library
and the tests with it, and runs CTest with ASLR disabled (TSan aborts with an
"unexpected memory mapping" on kernels with vm.mmap_rnd_bits above 28).
AKERR_SANITIZE takes any sanitizer list, so -DAKERR_SANITIZE=address,undefined
works the same way. CTest gives each test halt_on_error=1, so a report fails
the test rather than being printed and passed over — running a sanitized test
binary by hand does not inherit that. Set it yourself
(TSAN_OPTIONS=halt_on_error=1 ./build/tsan/test_err_threads_pool) or the
binary can print a race and still exit 0.
Mutation testing is available through:
cmake --build build --target mutation
scripts/mutation_test.py --target src/error.c --threshold 65
Code coverage is available through:
cmake --build build --target coverage
scripts/coverage.py --threshold 90 --branch-threshold 50
scripts/coverage.py configures its own instrumented build tree (default
build/coverage, -DAKERR_COVERAGE=ON), runs the CTest suite there, and
reports gcov line/branch coverage per library source. Thresholds gate each
file as well as the total. Coverage measures the library's own sources only --
src/error.c, src/lock.h and the generated src/errno.c; the public header's
macros expand at their call sites, so mutation testing is what checks those.
To build without threads (no locking, no thread-local storage, thread tests not
registered), configure with -DAKERR_THREADS=none. auto is the default and
fails the configure rather than falling back.
Coding Style & Naming Conventions
Use C99-compatible C and follow the surrounding style. Functions and types use
the akerr_ prefix; macros and constants use AKERR_ or all-caps macro names
such as PREPARE_ERROR. Keep generated-code changes in templates or generator
scripts, not in build outputs. Preserve concise comments for invariants,
macro constraints, and non-obvious error lifecycle behavior.
Locking. One recursive lock (akerr_state_lock, see src/lock.h) covers
both the error pool and the status registry. A function named *_locked is
called with that lock already held; a function without the suffix takes it.
Never take it in a body written with the FAIL_*_RETURN macros — those return
from the middle of the function and would skip the unlock. Split it instead: the
locked body does the work, and a thin wrapper takes the lock, calls it, and
releases it on the single return path. Do not call consumer code
(akerr_log_method, akerr_handler_unhandled_error) while holding it.
Exiting. Never call exit() with a status value. Call akerr_exit(), which
owns the one mapping from an akerr status to an exit code: an exit status is a
byte, and every consumer status starts at 256, so passing a status through
exit() silently truncates it — status 256 exits 0 and reports success. The
only exits that bypass it are the two that have no status to map:
ENSURE_ERROR_READY's pool-exhaustion abort in include/akerror.tmpl.h, and the
NULL-context case in akerr_default_handler_unhandled_error() in src/error.c. This rule applies to test
programs too, except where the test's whole point is to observe the raw
truncation.
Testing Guidelines
Add tests as tests/err_<behavior>.c. Register each new test in the
AKERR_TESTS list in CMakeLists.txt; tests that intentionally abort must
also be listed in AKERR_WILL_FAIL_TESTS. Prefer focused executable tests that
return zero on success and use existing helpers such as AKERR_CHECK. Run the
CTest suite before submitting changes, and run mutation testing and coverage
when changing core control-flow, reference counting, stack-trace, or handler
behavior.
Tests that drive the library from several threads go in the AKERR_THREAD_SAFE
branch of the AKERR_TESTS list — an -DAKERR_THREADS=none build has no
threading to test — and use tests/err_threads.h, which runs a body on
AKERR_TEST_THREADS threads that meet at a barrier first.
Count failures per thread with AKERR_TCHECK rather than returning early: a
thread that abandons its work leaves the others holding pool slots and turns one
failure into a cascade. Anything the test shares between its own threads must go
through __atomic builtins — a race in the test is still a race, and
ThreadSanitizer cannot tell you whose it is. The capturing logger in
err_capture.h is single-threaded (shared buffer, shared length); use
akerr_thread_logger instead. Run scripts/thread_test.sh when changing
anything that touches the pool, the registry, initialization, or the lock.
Commit & Pull Request Guidelines
Recent commits use short, imperative, sentence-case subjects, for example
Fix refcount leak and stack-trace buffer overflow. Keep commits focused and
describe the observable behavior changed. Pull requests should include a brief
summary, tests run, any compatibility impact for public macros, generated
headers, installation paths, or CMake/pkg-config consumers, and a link to the
issue they close.
Agent-Specific Instructions
Outstanding work goes in the issue tracker, not in a file. Open an issue at
https://source.starfort.tech/andrew/libakerror/issues — tea issues create --repo andrew/libakerror — naming the file and line, the functional
consequence, and what closing it would touch. Label it by kind and blast radius
and leave status::grooming on it until its scope and approach are settled.
Do not add outstanding items to TODO.md: that file is the record of why
the handler ladder is a major-version change, why a copied akerr_ErrorContext
is a trap, why validating more inputs lowers branch coverage, and why the
mutation score is a floor. A description of work still to do goes stale the
moment somebody does it.
This library's defects are most often found by its consumers, so make filing
them cheap. Three defects in this library — IGNORE() leaking a context, the
un-namespaced coverage target, and the missing akerrorConfigVersion.cmake —
were written down in libakstdlib's own notes and never here, so each was
worked around once per consumer and nobody saw the pattern. A consumer filing
upstream should cost them one issue instead of one workaround. libakgl,
libakstdlib and akbasic are all on the same forge.
Do not overwrite uncommitted user changes. Avoid editing generated files in
build/; update include/akerror.tmpl.h, src/error.c, CMake files, tests,
or scripts instead.