Skip to content

Commit 9897cf8

Browse files
committed
docs: make the API reference a gate, and write the paper trail (WP-15)
Doxygen wired to GitHub Pages is what most projects have and nobody reads. It builds, it deploys, and it is never wrong, because it never says anything: entities appear with their signature and no prose, new code lands undocumented, and the site absorbs it in silence until the reference is a searchable rendering of the headers, which the headers already were. So the reference is built as a gate first. Doxygen runs with WARN_AS_ERROR on every pull request, and a public entity added without a doc comment fails the pull request that added it. The setting that gives that teeth is not WARN_AS_ERROR and not WARN_IF_UNDOCUMENTED. It is EXTRACT_ALL = NO. With EXTRACT_ALL = YES -- the default in most hand-copied Doxyfiles -- Doxygen emits a page for every entity whether or not anyone documented it, and WARN_IF_UNDOCUMENTED goes permanently quiet. A project can have warnings-as-errors enabled and still be producing the empty reference described above, which is why it is worth naming. Turning the gate on found 86 undocumented public entities in code that had been through eight work packages and reads, casually, as well commented. The comments were there; they were on the interesting things. What was missing was the ordinary surface a caller meets first -- size(), empty(), translation(), the arithmetic operators on Vec3. Two of the 86 were bugs rather than omissions: - RevoluteJoint::upper had no comment because one comment above lower was describing both. Doxygen binds a comment to a single entity, so the published reference documented the lower limit and left the upper blank. - IkReport::orientation_error had the same shape, sharing a comment with position_error reading "position error in metres, and orientation error in radians". The rendered page said that about position_error alone. Neither is visible when reading the header, where the comment plainly covers both lines. That is also why a grep-for-/// lint was rejected as the cheaper alternative: in the source both members are preceded by a comment. Only something that parses the way Doxygen parses sees which one it binds to. WARN_NO_PARAMDOC and WARN_IF_INCOMPLETE_DOC are deliberately off. They demand an @PARAM and @return on every entity including size(), empty() and valid(), where the only text that satisfies the rule is "@return The size." A rule whose compliant output is that teaches contributors documentation is a form to fill in, and inflates every diff until reviewers skim the documentation hunks. The gate asks one thing instead: every public entity has a sentence saying what it is for. @PARAM is used where a parameter carries a unit, a frame or an ownership transfer, and not otherwise. A weaker gate on purpose -- the strongest one that cannot be satisfied by writing nothing of value. detail/ is excluded from the reference, which is an interface decision and not tidiness. A published symbol is one somebody may reasonably depend on, and the reference is where that promise is made. Publication is opt-in; the gate is not. Deploying needs Pages set to the "GitHub Actions" source, and on a private repository a plan that permits Pages at all. Neither is discoverable from inside a workflow, so a publish job that simply ran would fail on main -- a red cross on the default branch caused by a repository setting rather than by a commit. It is gated on the PUBLISH_DOCS repository variable, so enabling publication is a decision somebody takes rather than a default somebody has to notice and undo. Documentation quality is enforced from the first pull request regardless. The gate belongs to a Doxygen version, so it was verified against two: the runner's 1.9.8 and 1.15.0. Both build this configuration clean and both exit 1 on an undocumented entity, so the setup is not resting on one version's quirks. The docs job prints doxygen --version before building, so a failure after a runner-image bump can be told apart from a failure caused by the commit. Every markdown document a published page links to is itself an input, because otherwise Doxygen cannot resolve the link and the build fails. That turned out to be a feature: a broken cross-document link is now a build failure rather than a 404 somebody finds later. Also adds the rest of the paper trail: - docs/architecture.md, C4 at three levels. Drawing the component view from the real #include edges rather than from intent shows two subtrees rising from types.hpp that never meet: the pose side answers where, the motion side answers when, and nothing crosses. Trajectory planning here is over scalar axes and has no notion of a pose; kinematics has no notion of time. The consequence is that motionkit does not plan a Cartesian move, and WP-12 is exactly the module that would join them -- deferred, not overlooked. - CONTRIBUTING.md, CHANGELOG.md on Keep a Changelog, a pull request template, CODEOWNERS, and docs/review-checklist.md. The checklist holds only questions that have actually caught something here; a generic checklist produces a generic review. - CHANGELOG records that nothing has been released -- no tags exist -- rather than back-dating a release history onto commits that were never published. MOTIONKIT_BUILD_DOCS defaults to OFF, so Doxygen is not needed to build, test or consume the library. A contributor who never runs it is told by CI rather than blocked locally. No functional change: the header edits are comments. 139/139 green under GCC and Clang in Debug and Release, ASan/UBSan and clang-tidy, 128/128 under TSan, clang-format clean across 23 files, and the installed-package consumer still exits 0.
1 parent f2139af commit 9897cf8

20 files changed

Lines changed: 978 additions & 6 deletions

.github/CODEOWNERS

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Review is required from the code owner on every path.
2+
#
3+
# One owner on a single-maintainer project is a statement of responsibility
4+
# rather than a routing rule: it makes the reviewer explicit, and it means a
5+
# branch protection rule has something to require.
6+
7+
* @Onwcan
8+
9+
# Called out separately because a change here changes the meaning of every
10+
# other check in the repository, and should be read as such rather than
11+
# skimmed as configuration.
12+
/.github/workflows/ @Onwcan
13+
/.clang-tidy @Onwcan
14+
/.clang-format @Onwcan
15+
/CMakePresets.json @Onwcan
16+
/docs/adr/ @Onwcan

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## What this changes, and why
2+
3+
<!-- The diff shows what. Explain why, and what you considered instead.
4+
If this implements a decision worth keeping, it probably wants an ADR. -->
5+
6+
## Evidence
7+
8+
<!-- Numbers, not adjectives. "Sampling is allocation-free" is a claim;
9+
"asserted by TrajectoryRealtime.ProfileSamplingDoesNotAllocate over 1000
10+
calls with a positive control" is evidence.
11+
12+
If you changed something with a measurable cost, give the measurement. -->
13+
14+
## Risk
15+
16+
<!-- What breaks if this is wrong, and how would anyone find out?
17+
"Nothing, it is a doc change" is a fine answer. -->
18+
19+
---
20+
21+
Confirm before requesting review:
22+
23+
- [ ] `bash scripts/format.sh --check` passes
24+
- [ ] Tests added for the behaviour this changes — including the singular,
25+
empty or wrap-around case, which is where this library's bugs live
26+
- [ ] Public API has doc comments; `--target docs` builds clean
27+
- [ ] `CHANGELOG.md` updated under `Unreleased`, if behaviour or API changed
28+
- [ ] New realtime-callable functions have an allocation test
29+
- [ ] ADR added or updated, if a decision here would otherwise have to be
30+
reverse-engineered later

.github/workflows/ci.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,54 @@ jobs:
234234
-DCMAKE_PREFIX_PATH="$PWD/staged" -DCMAKE_BUILD_TYPE=Release
235235
cmake --build /tmp/consumer/build
236236
/tmp/consumer/build/consumer
237+
238+
# The API reference is a gate before it is a website. Doxygen runs with
239+
# WARN_AS_ERROR, so a public entity added without a doc comment fails here,
240+
# in the same pull request that added it -- see ADR-0009. Verified against
241+
# this runner's doxygen 1.9.8 and against 1.15.0 locally.
242+
docs:
243+
name: API reference builds clean
244+
runs-on: ubuntu-24.04
245+
steps:
246+
- uses: actions/checkout@v4
247+
- name: Install toolchain
248+
run: sudo apt-get update && sudo apt-get install -y ninja-build doxygen
249+
250+
- name: Record the Doxygen version
251+
# The gate's exact warning set belongs to a Doxygen version. Printing it
252+
# means a failure after a runner-image bump can be told apart from a
253+
# failure caused by the commit under test.
254+
run: doxygen --version
255+
256+
- name: Build the reference
257+
run: |
258+
cmake -S . -B build -DMOTIONKIT_BUILD_DOCS=ON -DMOTIONKIT_BUILD_TESTS=OFF
259+
cmake --build build --target docs
260+
261+
- name: Upload for publication
262+
uses: actions/upload-pages-artifact@v3
263+
with:
264+
path: build/docs/html
265+
266+
# Publication is opt-in, and off until the repository says otherwise.
267+
#
268+
# Deploying needs Pages set to the "GitHub Actions" source, and on a private
269+
# repository it needs a plan that allows Pages at all. Neither is knowable
270+
# from inside the workflow, so a job that simply ran would put a red cross on
271+
# main for a setting nobody had been asked about. Set the repository variable
272+
# PUBLISH_DOCS to "true" once Pages is configured, and this starts publishing.
273+
publish-docs:
274+
name: publish the reference
275+
needs: docs
276+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.PUBLISH_DOCS == 'true'
277+
runs-on: ubuntu-24.04
278+
permissions:
279+
pages: write
280+
id-token: write
281+
environment:
282+
name: github-pages
283+
url: ${{ steps.deployment.outputs.page_url }}
284+
steps:
285+
- name: Deploy to GitHub Pages
286+
id: deployment
287+
uses: actions/deploy-pages@v4

CHANGELOG.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented here.
4+
5+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## Versioning policy, and what 0.x means here
9+
10+
The project version is **0.1.0** and **nothing has been released**: there are no
11+
tags, and every entry below is unreleased. Saying so is more useful than
12+
back-dating a release history onto commits that were never published.
13+
14+
Under semver, `0.x` means the public API may change in any release. That is an
15+
accurate description of the current state and not a disclaimer to be ignored —
16+
`SerialChain` and `SynchronizedTrajectory` are both young enough that use will
17+
change them. Two things are nonetheless already firm, because they are cheap to
18+
keep and expensive to break:
19+
20+
- **Exported CMake target names.** `motionkit::core` and `motionkit::warnings`
21+
are stable, and CI builds a consumer against the installed package on every
22+
run so they cannot drift ([ADR-0004](docs/adr/0004-verify-the-installed-package-in-ci.md)).
23+
- **Frame naming.** `A_T_B` reads as "the pose of B in A" throughout, and
24+
composition cancels. Reversing that convention would silently invert
25+
transforms in caller code rather than fail to compile, so it will not change.
26+
27+
At 1.0.0 these become the ordinary semver promises. Until then, read a minor
28+
bump as "something may have moved".
29+
30+
## [Unreleased]
31+
32+
### Added
33+
34+
- **Forward and inverse kinematics for serial chains** (WP-03). `SerialChain`
35+
describes revolute joints by an axis and a point rather than DH parameters,
36+
computes forward kinematics as a product of exponentials, and solves the
37+
inverse by damped least squares with joint limits enforced. `IkReport`
38+
returns the closest approach to a singularity alongside the result.
39+
([ADR-0008](docs/adr/0008-kinematics-by-screws-and-a-damped-inverse.md))
40+
- **Stop planning from an arbitrary state** (WP-11). `StopProfile` plans a
41+
three-segment stop from any velocity and acceleration, including a state
42+
already outside the acceleration limit, and reports the stopping distance the
43+
safety envelope is built from.
44+
([ADR-0007](docs/adr/0007-stopping-is-planned-to-zero-acceleration.md))
45+
- **Jerk-limited trajectory planning** (WP-05). Seven-segment S-curve profiles,
46+
and `SynchronizedTrajectory` driving several axes from a single path
47+
parameter so they start and finish together.
48+
([ADR-0006](docs/adr/0006-jerk-limited-profiles-and-a-single-path-parameter.md))
49+
- **Frame graph** (WP-02). A named frame tree with allocation-free lookup
50+
routed through the lowest common ancestor.
51+
([ADR-0005](docs/adr/0005-frame-graph-is-a-tree.md))
52+
- **SO(3) and SE(3)** (WP-02). Quaternion-backed rotations with a canonical
53+
unit-norm invariant, renormalising composition, and conversions to matrix,
54+
rotation vector and Z-Y-X Euler angles.
55+
([ADR-0001](docs/adr/0001-quaternion-storage-for-so3.md),
56+
[ADR-0003](docs/adr/0003-well-conditioned-rotation-metric.md))
57+
- **Doxygen API reference**, built as a CI gate rather than only a website
58+
([ADR-0009](docs/adr/0009-the-api-reference-is-a-gate.md)). Publication to
59+
GitHub Pages is opt-in via the `PUBLISH_DOCS` repository variable.
60+
- **Architecture documentation** in [docs/architecture.md](docs/architecture.md):
61+
C4 context, container and component views, and the rules that decide where
62+
new code belongs.
63+
- **Build system, CI and packaging** (WP-01). GCC and Clang in Debug and
64+
Release, ASan/UBSan and TSan, clang-tidy and clang-format, and an installed
65+
package verified by building a consumer against it.
66+
([ADR-0002](docs/adr/0002-static-analysis-rule-set.md))
67+
68+
### Fixed
69+
70+
- `RevoluteJoint::upper` and `IkReport::orientation_error` were undocumented in
71+
the generated reference. In both cases a single comment above the preceding
72+
member appeared in the source to describe both, but bound only to the first.
73+
Found by turning on the documentation gate; invisible when reading the header.

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ option(MOTIONKIT_BUILD_TESTS "Build unit tests" ${MOTIONKIT_IS_TO
1616
option(MOTIONKIT_BUILD_ALLOCATION_TESTS
1717
"Build global-allocation probe tests" ON)
1818
option(MOTIONKIT_BUILD_BENCHMARKS "Build micro-benchmarks" OFF)
19+
option(MOTIONKIT_BUILD_DOCS "Build the Doxygen API reference" OFF)
1920
option(MOTIONKIT_ENABLE_WERROR "Treat warnings as errors" ON)
2021

2122
set(CMAKE_CXX_STANDARD 20)
@@ -74,6 +75,10 @@ if(MOTIONKIT_BUILD_TESTS)
7475
add_subdirectory(tests)
7576
endif()
7677

78+
if(MOTIONKIT_BUILD_DOCS)
79+
add_subdirectory(docs)
80+
endif()
81+
7782
if(MOTIONKIT_BUILD_BENCHMARKS)
7883
add_subdirectory(benchmarks)
7984
endif()

CONTRIBUTING.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Contributing
2+
3+
## Building
4+
5+
Requires CMake 3.24+, Ninja, and a C++20 compiler (GCC 14 or Clang 18 upward).
6+
Dependencies are fetched by CMake; there is nothing to install first.
7+
8+
```bash
9+
cmake --preset debug
10+
cmake --build --preset debug
11+
ctest --preset debug --output-on-failure
12+
```
13+
14+
Configure and build presets: `debug`, `release`, `asan`, `tsan`, `tidy`. Test
15+
presets: `debug`, `release`, `asan`, `tsan``tidy` has none, because it
16+
analyses while compiling rather than at test time. Each matches a CI job, so a
17+
preset passing locally means that job passes.
18+
19+
Two caveats worth knowing before you spend an afternoon on them:
20+
21+
- **TSan excludes the allocation tests.** They replace global `operator new`,
22+
which is exactly what TSan's runtime also does. `MOTIONKIT_BUILD_ALLOCATION_TESTS`
23+
is off under that preset, so the count is 128 rather than 139.
24+
- **`ScurveProfile` is rest-to-rest.** Planning to a position from a non-zero
25+
velocity is WP-12 and not implemented. `StopProfile` does start from an
26+
arbitrary state, which is a different problem.
27+
28+
## Before opening a pull request
29+
30+
```bash
31+
bash scripts/format.sh # or --check to only report
32+
cmake --preset tidy && cmake --build --preset tidy
33+
```
34+
35+
The `tidy` preset sets `CMAKE_CXX_CLANG_TIDY`, so the analysis runs as part of
36+
compiling; there is no separate test step for it.
37+
38+
CI runs GCC and Clang in Debug and Release, ASan/UBSan, TSan, clang-tidy,
39+
clang-format, an installed-package consumer build, and the documentation gate.
40+
All are required.
41+
42+
## The documentation gate
43+
44+
Every public entity needs a doc comment. Adding a public function without one
45+
fails CI — see [ADR-0009](docs/adr/0009-the-api-reference-is-a-gate.md).
46+
47+
```bash
48+
cmake -S . -B build-docs -DMOTIONKIT_BUILD_DOCS=ON
49+
cmake --build build-docs --target docs
50+
```
51+
52+
Doxygen is not needed for an ordinary build; the option defaults to `OFF`.
53+
54+
What the gate asks for is a **sentence saying what the entity is for**. It does
55+
not require `@param` and `@return` on everything, deliberately: `@return The
56+
size.` is not documentation. Use `@param` where the parameter carries a unit, a
57+
frame, or an ownership transfer.
58+
59+
One trap the gate exists to catch: a comment binds to **one** entity. This
60+
documents `lower` and leaves `upper` blank in the published reference, even
61+
though it reads as covering both:
62+
63+
```cpp
64+
/// Travel limits in radians.
65+
Scalar lower{-6.28};
66+
Scalar upper{6.28};
67+
```
68+
69+
## Style
70+
71+
`.clang-format` (Google, 90 columns) and `.clang-tidy` are authoritative — run
72+
them rather than reading this section. Conventions they cannot express:
73+
74+
- **Frames are named `A_T_B`**, read as "the pose of B expressed in A", so that
75+
`A_T_B * B_T_C` visibly cancels. A composition that does not cancel is a bug
76+
the reader can see.
77+
- **Units are SI** — metres, radians, seconds — and are never converted
78+
silently. Say the unit in the doc comment where a number has one.
79+
- **Failures a control loop can expect are `Expected<T, E>` values, not
80+
exceptions.** Exceptions are for a caller who has already broken a
81+
precondition, such as normalising a zero vector.
82+
- **Anything callable from the cyclic task allocates nothing and does not
83+
throw**, and there is a test in `tests/test_no_allocation.cpp` asserting it.
84+
If you add such a function, add the test too.
85+
- **Dependencies point down.** See the component diagram and the rules in
86+
[docs/architecture.md](docs/architecture.md). Siblings must not include each
87+
other.
88+
89+
## Tests
90+
91+
Property tests over generated inputs are preferred to hand-picked values where
92+
the property is the real claim — that `inverse()` undoes `forward()`, that
93+
`matrix()` is always in SO(3). Assert the property, not one case of it.
94+
95+
Two specific habits:
96+
97+
- **Test the singular case explicitly.** Most of the interesting failures in
98+
this library live at a singularity, a wrap-around, or a zero-length input.
99+
- **Give a negative control to any test that could pass by being inert.** The
100+
allocation counter has one — `TheAllocationCounterItselfWorks` — because a
101+
counter that never increments passes every test silently.
102+
103+
## Commits and ADRs
104+
105+
Commit messages explain **why**, in prose. The diff already shows what changed.
106+
107+
A decision that a future reader would otherwise have to reverse-engineer goes
108+
in an ADR under `docs/adr/`, numbered sequentially. Rejected alternatives are
109+
part of the record: the value is in knowing that an option was considered and
110+
why it lost, which is the question that gets asked eighteen months later.
111+
112+
Add an entry to [CHANGELOG.md](CHANGELOG.md) under `Unreleased` for anything
113+
that changes the public API or observable behaviour.

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Eigen, no KDL, no Pinocchio — the algorithms are the point.
2525
| WP-06 | Hand-eye, TCP and base-frame calibration | Planned |
2626
| WP-12 | Blending and TOPP (needs a position target from a non-zero state) | Planned |
2727
| WP-12 | CUDA batch IK and collision checking | Planned |
28+
| WP-15 | API reference, architecture docs, contribution process | **Done** |
2829

2930
139 tests, all passing under GCC and Clang in Debug and Release. ASan and UBSan
3031
exercise the full suite. TSan exercises the 128 ordinary tests; the eleven
@@ -309,6 +310,41 @@ test wrong.
309310
| clang-tidy, `--warnings-as-errors=*` | Rule set and exclusions justified in ADR-0002 |
310311
| `scripts/format.sh --check` with clang-format 18 | Formatting is not a review topic, and CI runs the same check developers run |
311312
| **install with repository tests off + downstream consumer compile and run** | Exercises only the installed package contract; it caught a real bug on first run when the exported target was `motionkit::motionkit_core` but consumers used `motionkit::core` |
313+
| **Doxygen with `WARN_AS_ERROR`** | A public entity added without a doc comment fails the pull request that added it. Turning it on found 86 gaps and two comments that bound to the wrong member — see [ADR-0009](docs/adr/0009-the-api-reference-is-a-gate.md) |
314+
315+
---
316+
317+
## Documentation
318+
319+
| Document | What it answers |
320+
|---|---|
321+
| [docs/architecture.md](docs/architecture.md) | C4 context, container and component views, and the rules that decide where new code goes |
322+
| [docs/adr/](docs/adr/) | Nine decisions, each with the alternatives that lost and why |
323+
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to build, what the gates are, and the conventions clang-format cannot express |
324+
| [docs/review-checklist.md](docs/review-checklist.md) | The questions that have actually caught something here |
325+
| [CHANGELOG.md](CHANGELOG.md) | What changed, and what `0.x` promises |
326+
327+
The API reference is generated from the headers and **enforced**, not merely
328+
published:
329+
330+
```bash
331+
cmake -S . -B build-docs -DMOTIONKIT_BUILD_DOCS=ON
332+
cmake --build build-docs --target docs
333+
```
334+
335+
Doxygen is not required for an ordinary build — the option defaults to `OFF`,
336+
and CI is what keeps the reference honest. Publication to GitHub Pages is
337+
opt-in via the `PUBLISH_DOCS` repository variable, because a deploy job that
338+
assumed Pages was configured would put a red cross on `main` for a repository
339+
setting rather than for a commit.
340+
341+
The single most useful thing to know before adding code here is in
342+
[docs/architecture.md](docs/architecture.md): the pose side (SO3, SE3,
343+
FrameGraph, SerialChain) and the motion side (MotionState, ScurveProfile,
344+
StopProfile) are two subtrees that **never meet**. Trajectory planning is over
345+
scalar axes and knows nothing about poses; kinematics knows nothing about time.
346+
Cartesian motion planning is the module that would join them, and it is
347+
deferred rather than missing.
312348

313349
---
314350

docs/CMakeLists.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# The API reference.
2+
#
3+
# Off by default: Doxygen is not needed to build, test or consume the library,
4+
# and a required tool that most contributors never invoke is a barrier for no
5+
# return. CI turns it on, which is what keeps it honest -- see ADR-0009.
6+
7+
find_package(Doxygen REQUIRED)
8+
9+
set(DOXYGEN_INPUT_DIR "${PROJECT_SOURCE_DIR}")
10+
set(DOXYGEN_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
11+
set(DOXYFILE "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile")
12+
13+
# configure_file rather than a checked-in Doxyfile, so PROJECT_NUMBER is the
14+
# CMake project version and cannot drift from it. A reference that reports the
15+
# wrong version is worse than one that reports none.
16+
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in" "${DOXYFILE}" @ONLY)
17+
18+
add_custom_target(docs
19+
COMMAND ${DOXYGEN_EXECUTABLE} "${DOXYFILE}"
20+
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
21+
COMMENT "Generating API reference (warnings are errors)"
22+
VERBATIM)

0 commit comments

Comments
 (0)