diff --git a/docs/superpowers/plans/2026-09-28-plot-docs-and-coverage.md b/docs/superpowers/plans/2026-09-28-plot-docs-and-coverage.md new file mode 100644 index 0000000..6e2d9e3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-28-plot-docs-and-coverage.md @@ -0,0 +1,160 @@ +# `plot` — documentation and a coverage floor + +**Goal:** Give the library a README and a user-facing documentation set, make the test coverage reproducible from one command, raise every library source above 80% line coverage, and add a floor check so it cannot silently regress. + +## Measured baseline (this is the starting point, not an estimate) + +`lcov` over `src/**`, built with `--coverage -O0 -g`, running the full 274-case suite: + +| | | +|---|---| +| Aggregate | **93.0% lines** (3206/3447), 97.8% functions (404/413) | +| Files captured | 23 | +| Below 80% | **`gl/loader.cpp` — 65.4% lines, 77.8% functions** | +| Near the line | `render/vertex_array.cpp` 83.1%, `gl/handle.cpp` 83.6% | +| At or near 100% | `data/*`, `math/transform`, `plot/axis`, `plot/measure`, `render/embedded_mono_font` | +| No instrumented lines | `core/status.cpp` (19 non-comment lines), `render/shaders.cpp` (82, string literals only), `text/stb_truetype_impl.cpp` (0) | +| Adapters | 2 files captured / 57 lines at 100% — the widget and host *bodies* need a window, so they are covered by the demo runs, not by unit tests; their testable seams (pointer anchors, row routing) are unit-tested | + +The uncovered lines are, in every case, **error paths and "loader unavailable" branches** — that is the honest description of the gap, and it is what the new tests must exercise. + +## Global Constraints + +- `plot_core` and every public header under `include/plot/` stay free of third-party code, and the public API must not change to make something testable. +- Public headers self-contained; the configure-time umbrella and self-containment checks keep passing. +- C++20; our targets clean under `-Wall -Wextra -Wpedantic -Wconversion -Wshadow`. +- No exceptions across the public API boundary. +- The existing 274 cases keep passing; no assertion is relaxed. +- **Tests must fail toward a false alarm, never toward a silent pass.** A coverage test that cannot fail is worse than none. +- **Never leave process-global GL state poisoned for later cases.** `plot::gl::load()` is one-way and process-wide; tests that need the loader unavailable must run in a **forked child** (the suite already has that pattern) or restore exactly what they stubbed. +- Documentation claims must be **checked against the code**, not written from memory: every API name, signature, default value and file path in the docs is verified before it is committed. +- Commit as soon as the tree builds; several agents in this phase have died of turn exhaustion. +- Every task ends with `cmake --preset linux-debug && cmake --build build/debug && ctest --test-dir build/debug --output-on-failure` green and warning-clean. + +--- + +### Task 1: Reproducible coverage, with a floor + +**Files:** +- Modify: `CMakePresets.json`, `CMakeLists.txt` +- Create: `tools/coverage.sh`, `tools/coverage_floor.py` (or an equivalent small checker) +- Create: `docs/coverage.md` (the measured table, regenerated by the script) + +**Interfaces:** +- Consumes: `ctest`, `lcov`, `genhtml`, the presets. +- Produces: a `linux-coverage` preset; `tools/coverage.sh` producing a summary, an HTML report and a machine-readable file; a floor check that exits non-zero when any `src/**` file is below 80% lines or the aggregate is below the configured target. + +- [ ] **Step 1: The preset** + +Add `linux-coverage` (Ninja, Debug, `--coverage -O0 -g` on compile and link, `binaryDir` `build/cov`). It must not affect the other presets, and `PLOT_BUILD_TESTS=ON` is required. Confirm `cmake --preset linux-coverage && cmake --build build/cov && ctest --test-dir build/cov` works from clean. + +- [ ] **Step 2: One command** + +`tools/coverage.sh` runs the suite, captures with `lcov`, writes `build/cov/coverage.info`, generates `build/cov/html/index.html` with `genhtml`, prints the per-file table for `src/**`, and **exits non-zero if the floor check fails**. It must be idempotent: a second run without changes gives the same numbers (say how you guarantee that — stale `.gcda` files are the usual cause). + +- [ ] **Step 3: The floor check** + +`tools/coverage_floor.py` reads the lcov file and fails if any file under `src/` is below **80% lines**, or the `src/` aggregate is below **90%**. It must be able to fail: prove it by temporarily raising the per-file floor above a file's actual coverage and showing the non-zero exit. State the numbers it enforces in `docs/coverage.md` so the contract is visible. + +- [ ] **Step 4: Wire it so it cannot rot** + +Decide how the check is reachable: a `coverage` custom CMake target, a documented command, or a ctest case that only registers in the coverage preset. Whichever you choose, it must not slow or destabilise the normal debug/asan/release runs, and the README's build section (Task 4) must name the exact command. Report what you chose. + +- [ ] **Step 5: Verify and commit** + +Report from a clean tree: the aggregate and per-file table, the floor check's exit status, and the deliberate-failure demonstration. Append the table to `docs/coverage.md`. +```bash +git add -A && git commit -m "test(coverage): a reproducible coverage preset, report and floor check" +``` + +--- + +### Task 2: Close the error-path gaps in the GL layer + +**Files:** +- Modify: `tests/test_gl_context.cpp`, `tests/test_render_smoke.cpp` (or a new `tests/test_gl_errors.cpp`) +- Modify: `src/gl/loader.cpp`, `src/render/vertex_array.cpp`, `src/gl/handle.cpp` **only if** a test requires a change, and then only a non-behavioural one (state it in the report) + +**Interfaces:** +- Consumes: the suite's existing stub-and-restore pattern for entry points (`RefuseGetIntegerv`, `RefuseMapBufferRange`, `RefuseFenceSync`), its fork-based death-test pattern, `plot::gl::available()`, `caps()`. +- Produces: tests, not new API. The public surface must not change. + +- [ ] **Step 1: `loader.cpp` — the extension scanner and the missing-symbol path (28 uncovered lines)** + +The uncovered lines are `has_token` (the whole function, including the `at_start`/`at_end` boundary), `has_extension`'s indexed path, the "no current context" return, and the post-failure `PLOT_ASSERT`. +Reach `has_token` **through the public path** by stubbing `glGetString` so `GL_VERSION` reports a `2.1` context and `GL_EXTENSIONS` returns a crafted string — then assert `caps().buffer_storage`/`persistent_map` both ways: extension present (`...GL_ARB_buffer_storage ...`) and a **prefix-only near-miss** (`GL_ARB_buffer_storageX`, which must be false). That near-miss is the boundary the function exists for, so it is the assertion that matters. Reach the indexed path by stubbing `glGetStringi` to return `nullptr` and to return a matching name. +For the missing-symbol path: run it in a **forked child** that calls `load()` with a stub loader returning `nullptr` for one required entry point, asserts `load()` returns false and `available()` is false, and exits with a status the parent checks. Never do this in the parent — `load()` is one-way and process-wide. + +- [ ] **Step 2: `handle.cpp` — the `kNoLoader` and "returned 0" families (42 uncovered lines)** + +The uncovered lines are the failure returns for `glCreateShader`/`glGenBuffers`/`glGenVertexArrays`/`glCreateProgram` returning 0, the `kNoLoader` returns, the program-link-failure log path, and two early returns. Cover the "returned 0" family by stubbing the specific entry point to return 0 (save and restore it), then asserting the constructor returns the documented `Status` **and leaves a usable object** (id 0, destructor safe). Cover the `kNoLoader` family in the forked child from Step 1, where `available()` is genuinely false. Cover the link-failure log path by compiling a program whose fragment shader is valid GLSL but fails to link against the vertex shader (a mismatched varying/interface is the usual way) and asserting the failure is reported with a non-empty log. + +- [ ] **Step 3: `vertex_array.cpp` — the failure and empty paths (10 uncovered lines)** + +The uncovered lines are the loader-unavailable returns, `vao_.reset()` on the failure path, and the `instances_ = 0` early returns for an empty batch. Cover the empty-batch returns through the public staging API (stage nothing, upload, draw — assert nothing is drawn and no GL error), and the failure returns through the forked child and the stub pattern. + +- [ ] **Step 4: Re-measure and report** + +Run `tools/coverage.sh` and report the new per-file table. **Every file under `src/` must be at or above 80% lines**, and the aggregate at or above 90%; if either fails, keep going rather than lowering the floor. Also report the aggregate change from 93.0% and name any line you could not reach and why (an unreachable line is worth stating, not hiding). + +- [ ] **Step 5: Commit** +```bash +git add -A && git commit -m "test(gl): cover the loader's extension scanner and the handle and vertex-array failure paths" +``` + +--- + +### Task 3: The user guide under `docs/` + +**Files:** +- Create: `docs/getting-started.md`, `docs/architecture.md`, `docs/embedding.md`, `docs/data-and-performance.md`, `docs/interaction.md`, `docs/styling.md`, `docs/building-and-testing.md` + +**Interfaces:** +- Consumes: the headers, the specs under `docs/superpowers/specs/`, the demos under `examples/`, the coverage table from Task 1. +- Produces: prose documentation for a *user* of the library. The `docs/superpowers/` tree stays as the design record and is not user documentation — say so once, in `docs/architecture.md`. + +- [ ] **Step 1: Write the pages, verifying every claim** + +For each page, the claims must come from the code: read the header and the relevant `src/` file before writing a sentence about it. Specifically: +- **getting-started** — prerequisites, the build/install/consume commands, and a minimal GLFW program that opens a window and draws one trace. The program must be **compiled** as part of the verification (a scratch build is fine; do not add it to `examples/` unless it duplicates nothing). +- **architecture** — the layering (`core`/`math` → `gl` → `data` → `render` → `plot` → `input`), what each layer owns, the dependency rule, and where the design record lives. +- **embedding** — one section per host (GLFW, ImGui, Qt6, GTK4), each with the exact `initialize`/`paint`/`resize`/event-translation steps the shipped adapter performs, the DPI/`ui_scale` rule, and the fact that the library never owns the context or the event loop. Include the per-host headless invocation used by the project's own verification. +- **data-and-performance** — `Series`/`ArraySeries`/`StreamSeries`, the SPSC contract and what a producer may and may not do, the min/max LOD pyramid and why it makes cost O(pixels), the exact-vs-interpolated query distinction (`reduce`/`sum`/`sumsq` exact over a range; the crosshair interpolated), and the measured frame times with the standing caveat that they are llvmpipe under Xvfb. +- **interaction** — cursors (two orientations), regions, measurement band, legend, crosshair, the `Controller` gesture table exactly as implemented, live follow with relative zoom and mouse panning, and the `XFollow`/`set_x_range` reinterpretation with its clamp. +- **styling** — every `Style` field with its default and whether it is logical or device, the `ui_scale` rule, the grid classes, the crosshair dash, and the `Legend`/`CrosshairStyle` knobs. +- **building-and-testing** — the presets, the options (`PLOT_BUILD_*`), how to run the suite, the headless demo modes, and the coverage command with the Task 1 floor. + +- [ ] **Step 2: No invented API** + +Any symbol you mention must exist with that exact name and signature. Check by grepping the headers, and for the getting-started program by compiling it. Report the check you ran per page. + +- [ ] **Step 3: Commit** +```bash +git add -A && git commit -m "docs: a user guide covering getting started, architecture, embedding, data, interaction, styling and testing" +``` + +--- + +### Task 4: The README, and a final accuracy pass + +**Files:** +- Create: `README.md` +- Modify: `docs/*.md` only to fix anything the final pass finds wrong + +**Interfaces:** +- Consumes: everything above, including Task 3's pages and Task 1's coverage table. +- Produces: the repository front door. + +- [ ] **Step 1: The README** + +Keep it to what a newcomer needs, in this order: what the library is in two sentences; the four host integrations and the headline capability (real-time, high sample rate, O(pixels) rendering) with one measured number; a short feature list; build and install; a ~20-line minimal embedding example; a link per doc page under `docs/`; the test and coverage commands with the current numbers; licence and third-party provenance (Adwaita Mono under OFL 1.1, vendored stb_truetype v1.26, and the `plot_core` zero-dependency rule); and a short, honest limitations list. +Do not market: no claims without a number or a file behind them. + +- [ ] **Step 2: The final accuracy pass** + +Walk every code snippet in the README and in `docs/` and compile or at least syntax-check it against the real headers. Fix what does not compile. Grep every mentioned symbol and confirm it exists. Report the list of symbols checked and any correction made. + +- [ ] **Step 3: Commit** +```bash +git add -A && git commit -m "docs: README with build, embedding and verification entry points" +```