Skip to content

Latest commit

 

History

History
171 lines (130 loc) · 100 KB

File metadata and controls

171 lines (130 loc) · 100 KB

Roadmap

Plan of work for pbr-cpp-memory-pool, grouped by milestone. Each item is numbered and checkbox-driven — every PR that completes a roadmap item flips the corresponding checkbox in the same PR. Completed items remain in the list as a permanent record.

Conventions: items are numbered <milestone>.<task>. New work that emerges mid-flight is appended at the end of its milestone with a fresh number; existing numbers are never reused or renumbered. Items that satisfy a clause of the functional/technical specification carry a (spec §X.Y) anchor so every line in git log is traceable to its contract.

The full requirements catalogue lives in docs/specs/01_spec_cpp_memory_pool.md. A consolidated mapping from spec sections to roadmap items is in the Spec Coverage Map at the bottom of this file.


Milestone 0 — Agent & Workflow Scaffolding

Goal: establish the documentation, agent configuration, source-tree shape, and quality bar before writing a single line of pool code.

  • 0.1 Persist the senior-C++-architect persona and English-only working language in the cross-tool agent configuration.
  • 0.2 Create AGENTS.md as the single source of truth for agent behavior.
  • 0.3 Add CLAUDE.md and GEMINI.md adapters that defer to AGENTS.md.
  • 0.4 Scaffold docs/ with README.md, adr/, specs/, and workflow/.
  • 0.5 Add ADR template, ADR index, and ADR-0001 ("Record architecture decisions").
  • 0.6 Document the git workflow (branches, Conventional Commits, PR template) in docs/workflow/git-workflow.md.
  • 0.7 Document the documentation-maintenance rules in docs/workflow/documentation.md.
  • 0.8 Create ROADMAP.md with numbered, checkbox-driven milestones (this file).
  • 0.9 Refresh README.md with project description, status, and pointers to AGENTS.md, ROADMAP.md, docs/.
  • 0.10 Relocate the initial spec to docs/specs/01_spec_cpp_memory_pool.md.
  • 0.11 Codify the enterprise quality bar (warnings-as-errors, sanitizers, Valgrind, clang-tidy, Doxygen) in AGENTS.md §10.
  • 0.12 Adopt the Maven-style cross-language source layout under src/main/cpp/it/d4np/memorypool/ — ADR-0002.
  • 0.13 Adopt the design-patterns policy and create the patterns catalogue — ADR-0003 + docs/patterns/README.md.
  • 0.14 Scaffold the production / test / benchmark source roots with placeholders and src/README.md.

Milestone 1 — Build System & Project Skeleton

Goal: a clean, reproducible build with empty stubs that compile, link, and run a no-op test under CTest — built against the Maven-style source tree.

  • 1.1 ADR: C++17 toolchain matrix (MSVC, GCC, Clang — Debug & Release) and supported platforms (spec §3.3) — see ADR-0005.
  • 1.2 Add CMakeLists.txt exposing src/main/cpp as the public include root; declare pbr_memory_pool library target with sources globbed under src/main/cpp/it/d4np/memorypool/.
  • 1.3 Add CMakePresets.json with debug, release, asan, ubsan, tsan presets.
  • 1.4 Add .clang-format (LLVM derivative, 4-space indent, 120-col soft limit) — ADR for style baseline. See ADR-0006.
  • 1.5 Add .clang-tidy with the baseline check set declared in AGENTS.md §9 — ADR if checks deviate from that baseline. See ADR-0006.
  • 1.6 Add src/main/cpp/it/d4np/memorypool/memory_pool.h (public C API skeleton, signatures from spec §5), memory_pool.hpp (C++ wrapper skeleton), and version.hpp (single source of truth for the project version constants consumed by CMake's project(... VERSION ...)) — no-op definitions, fully documented (spec §5; version constants per ADR-0004). Stub implementations live in memory_pool.cpp so the library is linkable from M1; real algorithms replace them in M2.
  • 1.7 Add CTest wiring; create a no-op smoke test under src/test/cpp/it/d4np/memorypool/. Framework choice in ADR-0007 (doctest v2.4.11 via FetchContent).
  • 1.8 Set up CI workflow: build matrix, clang-tidy, ASan + UBSan, CTest — gate master on all green. Implemented in .github/workflows/ci.yml per ADR-0005 §4: Linux × {GCC, Clang} × {Debug, Release, ASan, UBSan} + Windows × MSVC × {Debug, Release} + macOS × Apple Clang × {Debug, Release, ASan, UBSan}, plus clang-format repo-wide check and clang-tidy diff gate with --warnings-as-errors='*'.
  • 1.9 README quickstart: build / test commands verified on Windows and Linux. The README's Build and test section now spells out both the POSIX &&-chained one-liner and the Windows PowerShell variant (PS 5.1 has no &&), and points to the CI workflow as the canonical "verified on Windows and Linux" surface — the same three commands run daily on Linux × {GCC, Clang}, Windows × MSVC, and macOS × Apple Clang.
  • 1.10 ANSI C compatibility verification: dedicated CI job compiling memory_pool.h and a minimal C TU under -std=c89 -pedantic -Werror and -std=c99 -pedantic -Werror to enforce the C interop contract (spec §3.3). The minimal C consumer lives at src/test/c/it/d4np/memorypool/c_consumer_min.c (matches the cross-language layout per ADR-0002 — src/test/<lang>/...).
  • 1.11 Zero-external-dependency verification: CI job that builds with -nostdinc++ audit / find_package(...) introspection failing if any external package leaks into the build graph (spec §3.3). Configures with PBR_MEMORY_POOL_BUILD_TESTS=OFF so doctest is not fetched, then asserts no find_package in the library scope and inspects the resulting libpbr_memory_pool.a archive for stray third-party objects.
  • 1.12 Add the initial CHANGELOG.md at the repo root in Keep a Changelog 1.1.0 format. The Unreleased section is seeded with the user-visible changes accumulated across Milestones 0 and 1 (agent contract, source tree, build system, presets, header skeletons, code-style and static-analysis baselines, doctest test harness, the full enterprise CI workflow, ANSI C / C99 verification, zero-external-dependency audit, and the cross-platform README quickstart) so the Milestone 1.14 roll-up to [0.1.0] has real content. The "(once that file exists)" qualifier is also removed from the four places that referenced it (AGENTS.md PR body template + quality-bar table, .github/PULL_REQUEST_TEMPLATE.md, docs/workflow/git-workflow.md) — the file exists now, the rule is permanent (ADR-0004 §3).
  • 1.13 Add .github/workflows/release.yml triggered on v* tag push: re-run the full test matrix, build per-platform binaries (Linux x86_64, Windows x86_64, macOS arm64 — degrade gracefully where unavailable), emit SHA256SUMS, and create a draft GitHub Release with the corresponding docs/releases/v<X.Y.Z>.md as the body (ADR-0004 §4). Implemented via three jobs — verify (workflow_call into ci.yml for defense-in-depth re-verification of the tagged commit), build-artifacts (per-platform tar.gz packaging the static library + public headers + LICENSE + README + CHANGELOG), and release (download every artifact, emit SHA256SUMS, draft the GitHub Release with the body from docs/releases/<tag>.md). Pre-release suffixes (-alpha.N / -beta.N / -rc.N) are auto-detected and propagated to the GitHub Release. workflow_dispatch is exposed for idempotent re-runs (delete-then-recreate the draft) when an initial tag push needs to be replayed.
  • 1.14 Close Milestone 1 → v0.1.0: bump version.hpp to 0.1.0, roll CHANGELOG.md Unreleased into a [0.1.0] block with ISO date, add docs/releases/v0.1.0.md release notes, open the release PR for the maintainer to tag and publish (see docs/workflow/release.md). The version constants were set to 0.1.0 pre-emptively in Milestone 1.6 (under the assumption that M1 would close at v0.1.0), so the "bump" step is a no-op verification rather than a real edit; the CHANGELOG.md [Unreleased] block accumulated through Milestones 0 + 1 is rolled into [0.1.0] — 2026-06-10, the link references at the file's foot are rewritten ([Unreleased]compare/v0.1.0...HEAD, new [0.1.0]releases/tag/v0.1.0), and docs/releases/v0.1.0.md is added with human-prose release notes grouped by theme. The maintainer tags v0.1.0 from master after this PR merges and clicks Publish on the draft GitHub Release produced by release.yml.
  • 1.15 CMake configure-smoke CI workflow (.github/workflows/build-smoke.yml) — early subset of §1.8. Runs cmake --preset debug and --preset release on every PR touching CMake / sources / configs. Catches latent CMakeLists.txt and preset breakage (like the version-regex zero bug fixed in PR #6) before it reaches a fresh-clone consumer. Superseded by §1.8build-smoke.yml is removed in the same PR that introduces ci.yml; the full build matrix subsumes the smoke configure step.

Milestone 2 — Core Memory Pool (Single-Threaded MVP)

Goal: a correct, leak-free, O(1) fixed-block pool matching the spec — single-threaded, no dynamic growth yet, with measured demonstrative patterns.

  • 2.1 ADR: implicit free-list layout, block_size minimum (≥ sizeof(void*)), and alignment guarantee (spec §4, spec §2.1). Implemented as ADR-0009: the free list is implicit (next-pointer in first sizeof(void*) bytes of free slots) and initialised in ascending address order; block_size is strictly validated against block_size ≥ sizeof(void*) AND block_size % alignof(std::max_align_t) == 0 (no silent rounding — NULL on violation); block_count > 0 with a mandatory size_t overflow guard on block_size * block_count; backing storage allocated via C++17 ::operator new(size, std::align_val_t) so a single zero-external-dependency code path serves every Tier-1 platform; returned pointer alignment guarantee is alignof(std::max_align_t) (drop-in malloc parity). The struct memory_pool field list (backing, head, block_size, block_count, alignment) is fixed here so M2.7's foreign-pointer range check has the data it needs; where the struct lives (Pimpl vs in-file) is M2.2's call.
  • 2.2 ADR: introduce the RAII wrapper (Pool) and the Pimpl idiom across the C++/C boundary; update patterns catalogue. Implemented as ADR-0010: the C++ Pool is a move-only RAII owner of memory_pool_t* (ctor → memory_pool_create, dtor → memory_pool_destroy, copy deleted, single memory_pool_t* handle_ data member, sizeof(Pool) == sizeof(void*)); struct memory_pool is forward-declared in memory_pool.h and defined exclusively in memory_pool.cpp (C-style Pimpl — the C handle is the Impl, no separate Pool::Impl struct). Both patterns are co-introduced and interdependent, so they share a single ADR per AGENTS.md §8 #5. Catalogue updated: two new rows in docs/patterns/README.md Adopted / Planned, status Planned until M2.3 lands the body of struct memory_pool and the meaningful semantics of the C functions. Five rejected alternatives recorded (classical C++ Pimpl with unique_ptr<Impl>, embed-struct-as-member, shared_ptr<memory_pool>, deep-clone copyable Pool, pre-empt the C/C++ exception policy).
  • 2.3 Implement memory_pool_create and memory_pool_destroy with contiguous backing allocation (spec §2.1, spec §5, spec §3.1 — destroy releases all pre-allocated memory). Bodies land in memory_pool.cpp replacing the M1 stubs: struct memory_pool is defined (Pimpl per ADR-0010 — the C handle is the Impl), the three ADR-0009 §2 block_size preconditions and the block_count > 0 + size_t-overflow guard from §3 are enforced (NULL on violation, never silent rounding), the contiguous backing is obtained via ::operator new(total, std::align_val_t{alignof(std::max_align_t)}) with std::bad_alloc caught at the boundary, and the implicit free list is initialised in ascending address order using std::memcpy for strict-aliasing safety. memory_pool_destroy releases the backing through the matching aligned ::operator delete and then frees the metadata struct; NULL is a no-op. memory_pool_alloc / memory_pool_free remain M1 stubs — their O(1) bodies arrive in M2.4. Companion edits: memory_pool.h and memory_pool.hpp Doxygen spell out the three preconditions + the alignof(max_align_t) return-pointer alignment guarantee linking ADR-0009; pool_smoke_test.cpp is refactored with seven new TEST_CASEs covering the valid-args round-trip, destroy(nullptr) safety, and one case per precondition violation (block_size = 0, < sizeof(void*), misaligned, block_count = 0, overflow); the RAII wrapper tests are tightened to verify handle-transfer on move and source-becomes-empty after move. Patterns catalogue flips both RAII and Pimpl from Planned to Implemented.
  • 2.4 Implement memory_pool_alloc and memory_pool_free against the implicit free list — both O(1); alloc returns NULL on exhaustion in fixed mode (spec §2.2, spec §2.3, spec §5). Bodies in memory_pool.cpp replace the remaining M1 stubs: memory_pool_alloc is a constant-time pop of the implicit free-list head (block = pool->head_; pool->head_ = *static_cast<void**>(block); return block; with explicit nullptr returns on null pool and on exhausted pool); memory_pool_free is the symmetric constant-time push (*static_cast<void**>(block) = pool->head_; pool->head_ = block; with no-op guards for null pool and null block). The foreign-pointer / out-of-range detection is M2.7's concern and is documented as UB in the public Doxygen. Companion edits: pool_smoke_test.cpp gains six new TEST_CASEs (happy-path alloc, null-pool / null-block free no-ops, exhaustion + re-allocation, distinct + aligned pointer guarantee verifying the ADR-0009 §5 contract at runtime, Pool RAII wrapper allocate/deallocate LIFO round-trip) and drops the "still M1 stubs" TEST_CASE that has now served its purpose.
  • 2.5 Implement the C++ it::d4np::memorypool::Pool RAII wrapper. The wrapper's bodies (ctor → memory_pool_create, dtor → memory_pool_destroy, move-construct / move-assign, allocate / deallocate / native_handle forwarders) were co-introduced with the C-side implementation in M2.3 and M2.4 — see memory_pool.cpp. M2.5 is the formal acknowledgment that the minimal surface from ADR-0010 §2 is complete and exercised end-to-end by the smoke-test cases that landed across PRs #18 and #19 (construction, invalid construction → empty wrapper, move-construct handle transfer, move-assign handle release, allocate / deallocate LIFO round-trip). Companion edit: memory_pool.hpp Doxygen drops the now-stale "Milestone 2.2 (ADR pending)" and "function bodies arrive together with the C implementation in Milestone 2" qualifiers, replacing them with concrete ADR-0010 references and a one-paragraph layout note (sizeof(Pool) == sizeof(void*), single memory_pool_t* handle_ member, copy deleted, move leaves source in valid empty state).
  • 2.6 ADR + impl: Factory Method / Builder for constructing configured pool instances (block size, block count, future growth/threading knobs). Implemented as ADR-0011 covering both patterns (co-introduced and interdependent — Builder's build() is the natural caller of Factory Method's make, per AGENTS.md §8 #5). static std::optional<Pool> Pool::make(block_size, block_count) is the Factory Method — engaged optional on success, std::nullopt on any ADR-0009 §2/§3 failure, orthogonal to the M3.1 std::bad_alloc-throwing decision. class PoolBuilder is the Builder — fluent .with_block_size().with_block_count().build() returning std::optional<Pool> via Pool::make; const build() so the same configured builder can produce independent pools. The Pool ctor stays as the lower-level path with its silent-empty-state semantic from ADR-0010 §2. Catalogue gains two new Adopted / Planned rows (status Implemented). Seven new TEST_CASEs in pool_smoke_test.cpp cover the happy path, three failure paths (misaligned, zero count, default-constructed builder, partially-configured builder), and the multi-build property of the const build(). Six rejected alternatives recorded in the ADR (Factory returning Pool directly, throwing factory, std::variant for error categories, skip-Factory, skip-Builder, nested Builder).
  • 2.7 Correctness tests covering the three scenarios named in spec §6.1: full exhaustion, null inputs, foreign-pointer / out-of-pool-range pointer policy. Full exhaustion and null inputs were already covered by the M2.3 / M2.4 smoke tests (memory_pool_alloc exhausts the pool after block_count successful pops, memory_pool_destroy(NULL) is a defined no-op, memory_pool_alloc returns NULL on a null pool, memory_pool_free is a no-op on null pool or null block). The new work in M2.7 is the foreign-pointer / out-of-range scenario, formalised as ADR-0012memory_pool_free runs an O(1) range + alignment check against the pool's backing extents (ADR-0009 §6 fields) and silently no-ops on foreign / out-of-range / misaligned pointers. The comparison uses std::uintptr_t arithmetic to avoid [expr.rel]/4 unspecified behaviour on cross-allocation pointer <. The C++ Pool::deallocate wrapper inherits the policy through its forward to memory_pool_free. Five new TEST_CASEs exercise out-of-range below, out-of-range above, in-range misaligned, foreign heap pointer (from a sibling pool), and stack pointer — each verifying the pool state is bit-identical before and after the offending call. Double-free detection is explicitly deferred to Milestone 6 (Decorator).
  • 2.8 Valgrind job in CI gated on ERROR SUMMARY: 0 errors from 0 contexts (spec §3.1, spec §6.2). Carry the exact gcc -g -O0 ... && valgrind --leak-check=full --show-leak-kinds=all ./test_pool invocation from spec §6.2 as a literal demonstrative test under src/test/cpp/it/d4np/memorypool/spec_6_2_valgrind/ so the spec-named verification path is reproducible 1:1. Implemented in .github/workflows/ci.yml as the valgrind job on Ubuntu 24.04: installs Valgrind via apt, compiles test_pool.c under gcc -std=c89 -pedantic -g -O0 and memory_pool.cpp under g++ -std=c++17 -g -O0 (the structural C → C++ substitution required because the implementation is C++17 per ADR-0009 §1; the side-by-side mapping with the literal spec command is in src/test/cpp/it/d4np/memorypool/spec_6_2_valgrind/README.md), links with g++, then runs valgrind --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=definite,indirect --error-exitcode=1 and additionally greps the output for the literal spec success criterion. The --errors-for-leak-kinds=definite,indirect flag is the only non-spec addition — without it, a leaked backing buffer would pass --error-exitcode while violating spec §3.1; still reachable and possible stay informational so global libstdc++ state does not trip the gate. The test_pool.c binary is also registered as a CTest target (spec_6_2_valgrind) so the same scenario is exercisable locally without Valgrind under the standard CMake / CTest invocation; it exercises every spec §6.1 scenario that intersects Valgrind's surface (happy path, null inputs, exhaustion, foreign-pointer probes from heap and stack) plus a two-cycle full alloc / free round-trip so the implicit free list is rebuilt under audit before memory_pool_destroy releases the backing.
  • 2.9 Microbenchmark vs malloc/free over 1,000,000 iterations under src/bench/cpp/it/d4np/memorypool/ (spec §6.3); numbers committed and summarised in the README. Implemented as pool_vs_malloc_bench per ADR-0014: hand-rolled std::chrono::steady_clock timing on two scenarios (bulk — alloc N then free N; interleaved — alloc + immediate free × N), 1,000,000 iterations × 10 repeats per scenario with the first repeat discarded as warm-up, 64-byte blocks by default, statistical summary (min / median / mean / max / stddev) plus a headline malloc_median / pool_median ratio per region. Per-iteration volatile byte write + a portable do_not_optimize barrier defeat Release-mode dead-code elimination on both allocators identically. The binary is built off by default; the new bench preset (Release + PBR_MEMORY_POOL_BUILD_BENCHMARKS=ON + PBR_MEMORY_POOL_BUILD_TESTS=OFF) opts in. A bench-smoke CI job on Ubuntu 24.04 builds the binary and runs it with --iterations 10000 --repeats 3 — exit-code gate only, no numeric thresholds, because shared GHA runners are too noisy for that gate to be meaningful (ADR-0014 §8). Canonical numbers for the v0.2.0 release are committed at docs/bench/v0.2.0-windows-msvc-x64.md (Intel Core i5-6600K Skylake @ 3.5 GHz, MSVC 19.51, Release): pool is 11.02 × faster on bulk-alloc, 5.35 × on bulk-free, 4.45 × on interleaved. The README Performance section carries the headline table; the methodology / scenario rationale / six rejected alternatives are in ADR-0014. Spec Coverage Map §6.3 moves from ⏳ to 🚧 — single-threaded coverage is complete; full ✅ arrives with the M4.5 concurrent comparative rerun that the same binary will host once threading lands.
  • 2.10 Metadata-overhead measurement and budget: instrumented test reports bytes of pool-internal metadata as a function of block_count; result documented in an ADR and asserted as a CI lower bound (spec §3.2). Implemented as ADR-0015 and a new public C function size_t memory_pool_metadata_bytes(const memory_pool_t*) added to memory_pool.h; the C++ side gains the [[nodiscard]] std::size_t Pool::metadata_bytes() const noexcept forwarder in memory_pool.hpp. Per-pool metadata is sizeof(struct memory_pool) — currently 40 bytes on every Tier-1 64-bit host (the five ADR-0009 §6 fields, no padding); the budget is set to 128 bytes with 88 bytes of headroom, gated at compile time via static_assert(sizeof(memory_pool) <= 128U, ...) in memory_pool.cpp (fires on every cell of the 14-cell build matrix) and at runtime via four new TEST_CASEs in pool_smoke_test.cpp covering null-pool no-op, live-pool sanity, budget assertion, and O(1)-in-block_count (1024-block vs 1,000,000-block pools report identical values). Per-block external metadata is 0 bytes by construction (the implicit free list per ADR-0009 §1 stores next-free links in the unused storage of free blocks); ADR-0015 §1 records the accounting and ADR-0015 §4 fixes the renegotiation protocol for any future milestone that needs to cross the 128-byte line. Six rejected alternatives recorded — no introspection (cannot CI-gate the value), per-block bitmap (re-introduces O(block_count) overhead), class-level constant (locks consumers to compile-time struct size, breaks ABI stability), tighter budget (churn), looser budget (defeats documentation value), runtime-only or compile-time-only gates (the duplication is deliberate — they catch divergent regression classes). The new memory_pool_metadata_bytes declaration is held to the same ANSI C C89 compatibility contract as the rest of the public C API and is exercised by c_consumer_min.c under the M1.10 -std=c89 -pedantic -Werror and -std=c99 -pedantic -Werror CI jobs. Spec Coverage Map §3.2 flips from ⏳ to ✅.
  • 2.11 Close Milestone 2 → v0.2.0: bump version.hpp, roll CHANGELOG.md, draft docs/releases/v0.2.0.md, open release PR (ADR-0004 §2). version.hpp is bumped to MAJOR=0 MINOR=2 PATCH=0 and the PBR_MEMORY_POOL_VERSION_STRING constant to "0.2.0" in lockstep; the pool_smoke version-check TEST_CASE is updated to assert the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [0.2.0] — 2026-06-11 block consolidating every M2 entry into a flat Added + Changed + Spec Coverage Map flips structure, with the bottom-of-file link references rewritten ([Unreleased]compare/v0.2.0...HEAD, new [0.2.0]releases/tag/v0.2.0). docs/releases/v0.2.0.md carries the human-prose release notes that release.yml consumes as the GitHub Release body. README.md status badge, status paragraph, and milestone table are refreshed (badge from yellow v0.1.0 build skeleton to green v0.2.0 single-thread MVP; Milestone 2 row from ⏳ next to ✅ complete; Milestone 3 row from ⏳ planned to ⏳ next). The maintainer reviews and merges this PR, then the agent tags v0.2.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced by release.yml.

Milestone 3 — C++ Wrapper & Type Safety

Goal: an idiomatic C++17 wrapper around the C core, with RAII and allocator-aware ergonomics.

  • 3.1 ADR: exception policy at the C / C++ boundary — NULL on the C side, std::bad_alloc on the C++ side, behind a configurable knob (spec §2.2 — "return NULL (or throw an exception in C++)"). Implemented as ADR-0016: the C ABI is exception-free forever (every C failure is NULL / no-op, formalising ADR-0005 §3); the C++ side adopts a dual-verb surface where the "configurable knob" is resolved per call site, not per build — allocate() throws std::bad_alloc on exhaustion (and on a moved-from wrapper), the new try_allocate() is noexcept and returns nullptr with the exact v0.2.0 semantics. The Pool ctor now throws std::bad_alloc on construction failure (amends ADR-0010 §2's silent empty state — recorded on its Status line); Pool::make / PoolBuilder::build remain the non-throwing path, restructured around a private adopt-handle ctor so no try/catch is involved. The benchmark's timed loops switch to try_allocate() (apples-to-apples with malloc's in-band NULL; byte-identical to the code path that produced the committed v0.2.0 numbers). Six rejected alternatives recorded (compile-time macro fork, policy template parameter, allocate_or_throw naming inversion, std::invalid_argument split, std::expected clone, runtime per-pool flag). Five new / updated TEST_CASEs cover ctor-throws, allocate-throws-on-exhaustion, try_allocate-nullptr-on-exhaustion, and the moved-from wrapper behaviour of both verbs.
  • 3.2 it::d4np::memorypool::TypedPool<T> template with RAII lifetime and try_allocate / allocate variants. Implemented per ADR-0017 as a header-only template in typed_pool.hpp composing the untyped Pool: the spec-conformant block_size is derived at compile time (max(sizeof(T), sizeof(void*)) rounded up to the alignof(std::max_align_t) multiple) so every ADR-0009 §2 precondition holds by construction, and over-aligned T is rejected with a static_assert instead of silently receiving under-aligned storage. The surface is two-layer (ADR-0017 §3): typed storage verbs allocate / try_allocate / deallocate follow the ADR-0016 dual-verb policy verbatim, and the object-lifetime pair construct(Args&&...) / destroy placement-news / destroys a T with the strong exception guarantee on a throwing T ctor (the slot returns to the free list before the exception propagates). RAII / move-only lifetime, the throwing ctor, and the non-throwing TypedPool::make Factory Method are inherited structurally from Pool through composition. Eight new TEST_CASEs in a dedicated typed_pool_test CTest binary cover compile-time block-size properties, dual-verb exhaustion, the full construct / destroy lifecycle with an instrumented type, the strong guarantee with a deliberately-throwing type, both construction paths, and the moved-from shell. Six rejected alternatives recorded (inheritance from Pool, raw sizeof(T) block size, silent over-aligned support, lifetime-only surface, try_construct, unique_ptr + deleter factory — deferred to the M6 ergonomics wave).
  • 3.3 ADR + impl: Adapter — STL-compatible allocator over the underlying pool; propagation traits specified in the ADR. Implemented per ADR-0018 as the header-only template pool_allocator.hpp: it::d4np::memorypool::PoolAllocator<T> satisfies the Cpp17Allocator requirements and is the structural Adapter bridging the pool's fixed-block void* interface to the variable-size std::allocator_traits contract. It is a non-owning back-reference to a Pool (single Pool* member, sizeof == sizeof(void*); the pool must out-live every container and adapter copy, the std::pmr::polymorphic_allocator lifetime contract). Routing (ADR-0018 §2): a request routes to the pool iff n == 1 && sizeof(T) <= pool.block_size() && alignof(T) <= alignof(std::max_align_t) — served by Pool::allocate (O(1), std::bad_alloc on exhaustion per ADR-0016 §2); everything else (n > 1, oversized / over-aligned T, rebound nodes larger than the block) delegates to over-aligned ::operator new / ::operator delete with a size_t-overflow guard. Because the standard guarantees deallocate(p, n) receives the same n/type as the matching allocate, and block_size() is invariant for the pool's lifetime, the routing predicate evaluates identically at allocate and deallocate — every pointer is freed by exactly the path that allocated it, with zero per-pointer bookkeeping. So std::list / std::map / std::set run entirely on the pool fast path while std::vector runs on the fallback; both are memory-correct. Propagation traits (ADR-0018 §4, specified as the roadmap item requires): propagate_on_container_copy_assignment, propagate_on_container_move_assignment, and propagate_on_container_swap are all std::false_type; is_always_equal is std::false_type (stateful); operator== is true iff the two adapters reference the same Pool; select_on_container_copy_construction keeps the default (copies retain the back-reference). A new introspection accessor size_t memory_pool_block_size(const memory_pool_t*) (ADR-0018 §3 — the O(1), NULL-tolerant companion to memory_pool_metadata_bytes, held to the same ANSI-C C89 contract and exercised by c_consumer_min.c) backs the size-fit decision, with a [[nodiscard]] std::size_t Pool::block_size() const noexcept forwarder. Seven new TEST_CASEs in a dedicated pool_allocator CTest binary cover pool-fast-path exhaustion, multi-block + oversized-T fallback (pool left untouched), equality / statefulness / rebinding, the propagation-trait static_asserts, and end-to-end std::list (pool path) + std::vector (fallback) round-trips. Seven rejected alternatives recorded (owning allocator, no-fallback, fallback-on-exhaustion, per-pointer bookkeeping, document-size-fit-as-caller-responsibility, propagate-all-true, std::pmr::memory_resource subclass — the last deferred). The comprehensive container matrix is M3.5.
  • 3.4 ADR + impl: Iterator (read-only) over the free list for diagnostics — disabled in release builds unless explicitly enabled. Implemented per ADR-0019 as the header-only free_list_iterator.hpp: FreeListIterator is a LegacyForwardIterator walking the implicit free list (ADR-0009 §1) — value_type is const void* (a free-slot address), operator* returns a reference to the iterator's own current-slot member (so it is a genuine forward, not input-only, iterator), the default-constructed iterator is the end sentinel; FreeListView is the range adaptor (begin() / end(), constructible from a const memory_pool_t* or a Pool&) enabling range-for, std::distance, std::find, etc. Gating (disabled in release unless explicitly enabled): the entire diagnostic surface sits behind the PBR_MEMORY_POOL_DIAGNOSTICS macro, which defaults to 1 when !NDEBUG (debug) and 0 when NDEBUG (release), any explicit definition winning; the CMake option PBR_MEMORY_POOL_ENABLE_DIAGNOSTICS (default OFF) is the documented opt-in, forcing the macro to 1 as a PUBLIC compile definition on the library target so the library and every linking consumer agree. Traversal encapsulation: the iterator delegates to three gated, NULL-tolerant, C89-clean C accessors — memory_pool_debug_free_list_head, memory_pool_debug_free_list_next, memory_pool_debug_free_count — that keep the next-link layout knowledge inside memory_pool.cpp, honouring the ADR-0010 Pimpl boundary instead of re-encoding ADR-0009 §1 in a public header; next reads the link via a static_cast<void* const*> that provably casts away no const. The accessors are exercised by c_consumer_min.c under the M1.10 C89/C99 jobs (guarded by the same macro). A dedicated free_list_iterator CTest binary carries five cases when diagnostics are on (ascending strided walk of a fresh pool with free_count vs std::distance cross-check, alloc-shrinks-list, freed-block-returns-to-head LIFO, exhausted-pool empty range, LegacyForwardIterator behaviours) and a single placeholder case when gated out, so the binary builds in every configuration. Six rejected alternatives recorded (always-compile the C accessors, re-encode the layout in the header, runtime flag, C-side iterator handle, mutable iterator, input-iterator proxy operator*).
  • 3.5 Tests against std::vector, std::list, and a small custom container. Implemented as the dedicated container_integration CTest binary container_integration_test.cpp, driving the M3.3 PoolAllocator<T> (ADR-0018) end-to-end through three container families: std::list (node-based, n == 1 → pool fast path; where the ADR-0019 diagnostics surface is enabled, the tests assert each node really comes from the pool by watching the delta in memory_pool_debug_free_count across pushes and clear, a measurement robust to any implementation-defined sentinel node), exercised with both int and std::string elements (the latter validating non-trivial construct/destroy under ASan/Valgrind); std::vector (contiguous n > 1 → heap fallback; checked for contents, growth past the pool's capacity, copy via select_on_container_copy_construction, and <algorithm> interop, not pool occupancy); and a small custom container ForwardList<T, Allocator> — a minimal hand-written allocator-aware singly-linked list that rebinds the allocator to its node type and routes every node allocation and object lifetime through std::allocator_traits, proving the adapter works with an arbitrary conforming container, not just the standard ones. The custom container is driven with both std::allocator (generic, pool-agnostic) and PoolAllocator (with the guarded free-count assertion and a live-instance Tracked type proving destroy runs on every element). Seven TEST_CASEs total. The pool-sizing recipe the ADR-0018 §3 consequences deferred to this item is documented as a worked, compilable example in the test file's header comment: a node-based container's pool needs block_size ≥ sizeof(rebound node) (always larger than sizeof(T)), so pick a generous block_size (128 bytes here) — an undersized pool degrades safely to the heap fallback rather than corrupting memory. The comprehensive README usage section remains M7.2.
  • 3.6 Close Milestone 3 → v0.3.0: bump version.hpp, roll CHANGELOG.md, draft docs/releases/v0.3.0.md, open release PR (ADR-0004 §2). version.hpp is bumped to MINOR=3 PATCH=0 and PBR_MEMORY_POOL_VERSION_STRING to "0.3.0" in lockstep; the pool_smoke version-check TEST_CASE is updated to assert the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [0.3.0] — 2026-06-13 block (milestone headline + the M3.1–M3.5 Added / Changed subsections + a no-flip Spec Coverage Map note), with the bottom-of-file link references rewritten ([Unreleased]compare/v0.3.0...HEAD, new [0.3.0]releases/tag/v0.3.0). docs/releases/v0.3.0.md carries the human-prose release notes that release.yml consumes as the GitHub Release body. README.md status badge (green v0.2.0 single-thread MVP → green v0.3.0 C++ wrapper & type safety), status paragraph, and milestone table (Milestone 3 ⏳ next✅ complete, Milestone 4 ⏳ planned⏳ next) are refreshed. The maintainer reviews and merges this PR, then the agent tags v0.3.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced by release.yml.

Milestone 4 — Thread-Safe Variant

Goal: opt-in thread safety with a measurable single-threaded fast path preserved (spec §2.4).

  • 4.1 ADR: thread-safety Strategy (lock-free CAS vs. mutex vs. per-thread caches) and configuration knob (compile-time macro per spec §2.4). Decided in ADR-0020: thread safety is the GoF Strategy pattern bound at compile time (policy-based, not runtime-virtual) so the single-threaded build pays literally nothing (no atomic, no branch, no indirect call) — honouring spec §2.4's "preserve the single-thread fast path." Three policies, selected by the PBR_MEMORY_POOL_THREAD_SAFETY macro (default …_NONE, plus …_MUTEX and …_LOCKFREE) via a CMake option and fixed for the whole library at build time (a per-pool runtime flag is rejected — it would re-introduce the hot-path branch §2.4 forbids): SingleThreadedPolicy (the v0.3.0 path verbatim), MutexPolicy (a std::mutex across the O(1) head pop/push), LockFreePolicy (a Treiber-stack CAS loop on an ABA-tagged head — {ptr, tag} via double-width CAS, platform-conditional lock-freedom, degrading honestly to a locked impl where DWCAS is unavailable). Per-thread caches are deferred (a larger subsystem; the Strategy seam keeps them a non-breaking future addition). The macro mirrors the ADR-0019 gate-macro pattern. Flagged for M4.3: MutexPolicy grows struct memory_pool (≈ 80 bytes for a Windows std::mutex) and must keep the ADR-0015 static_assert(sizeof(memory_pool) <= 128) green or renegotiate per ADR-0015 §4. Seven rejected alternatives recorded (runtime-virtual Strategy, per-pool runtime flag, always-on locking, lock-free-only, mutex-only, per-thread caches now, untagged Treiber stack). No source changes in this PR — the policy classes, macro, and CMake option are implemented in M4.2 (Template Method skeleton) / M4.3.
  • 4.2 ADR + impl: Template Method allocation skeleton with hook points for the chosen Strategy. Implemented per ADR-0021: memory_pool_alloc / memory_pool_free are refactored into the alloc_skeleton / free_skeleton Template Method templates in memory_pool.cpp, which own the invariant frame — the race-free null-pool / null-block / foreign-pointer guards (ADR-0012, reading only post-creation-immutable fields, kept outside any lock) — and delegate the synchronized free-list head mutation to two compile-time policy hook points, Policy::pop_head / Policy::push_head. The exhaustion test (head_ == nullptr) lives inside pop_head, not the skeleton, so the future LockFreePolicy can re-test the head on every iteration of its CAS retry loop (the design pivot that makes one skeleton + two hooks fit all three ADR-0020 policies). Milestone 4.2 ships only SingleThreadedPolicy (the v0.3.0 head pop/push verbatim, no synchronization) and hard-wires it via using ActivePolicy = SingleThreadedPolicy;; the policy and skeletons live in the anonymous namespace of the TU, so the public C ABI, the C++ wrapper, struct memory_pool, and the ADR-0015 metadata budget are all unchanged, and the single-threaded build inlines to byte-identical v0.3.0 code. Behavior is unchanged — the full existing CTest suite (alloc / free / exhaustion / foreign-pointer across all six binaries) is the behavioral regression gate. M4.3 adds MutexPolicy / LockFreePolicy beside SingleThreadedPolicy and turns ActivePolicy into a PBR_MEMORY_POOL_THREAD_SAFETY-selected alias without touching the skeleton. Six rejected alternatives recorded (RAII acquire/release guard hooks, exhaustion check in the skeleton, foreign-pointer guard inside the policy, full-Strategy with guards in the policy, runtime-virtual hooks, separate skeleton per policy family).
  • 4.3 Implementation behind a compile-time switch; default remains single-threaded so the fast path is preserved (spec §2.4 — "configurable via a compile-time macro to maximize single-thread performance"). Implemented per ADR-0020 on the M4.2 ADR-0021 skeleton, without touching the skeleton: memory_pool.cpp now defines all three policies, compiling exactly the one selected by PBR_MEMORY_POOL_THREAD_SAFETY (a #if … #elif … #else #error block aliasing it to ActivePolicy) — SingleThreadedPolicy (NONE, default, the v0.3.0 head pop/push verbatim), MutexPolicy (MUTEX, a std::mutex held across the O(1) pop/push; the ADR-0012 foreign-pointer guard runs in the skeleton outside the lock), and LockFreePolicy (LOCKFREE, a Treiber-stack compare_exchange_weak loop on an ABA-tagged std::atomic<TaggedHead> head — in-slot next-links stay plain pointers, only the head is atomic, the tag defeats ABA, and the 3-argument CAS gives acq_rel success / derived-acquire failure). struct memory_pool gains the policy's state conditionally — a 16-byte std::atomic<TaggedHead> head under LOCKFREE, a std::mutex mutex_ under MUTEX — and the ADR-0015 static_assert(sizeof(memory_pool) <= 128) stays green in all three modes (verified: MSVC std::mutex lands the MUTEX struct at ~120 bytes, within budget). The macro constants + NONE default live in memory_pool.h (NOLINTBEGIN/END for cppcoreguidelines-macro-usage, C89-clean); the CMake option PBR_MEMORY_POOL_THREAD_SAFETY (NONE|MUTEX|LOCKFREE) maps to a PRIVATE compile definition on the library (it changes only memory_pool.cpp's internal policy, not the public ABI). create seeds the head per policy; the diagnostic accessors load the atomic head under LOCKFREE. A new thread-safety CI job builds + runs the full single-threaded CTest suite under MUTEX and LOCKFREE on Linux GCC + Clang (the build-correctness gate for the switch; concurrent stress + TSan are M4.4). The C ABI, the C++ wrapper, the public headers, and the single-thread fast path (NONE inlines to byte-identical v0.3.0 code) are all unchanged. No new ADR — the design is ADR-0020/0021; this item is the implementation. All six CTest binaries pass in NONE, MUTEX, and LOCKFREE locally (MSVC 19.51).
  • 4.4 Concurrent stress tests; TSan job added to CI. Implemented as the concurrency_stress CTest binary concurrency_stress_test.cpp, which drives Pool from THREAD_COUNT (8) threads to validate the MutexPolicy / LockFreePolicy implementations (ADR-0020) on three invariants: no over-vend / distinctness (a concurrent drain hands out exactly block_count blocks, every one distinct), full recovery / no leak (after 8 × 20 000 alloc/free churn the pool vends exactly block_count distinct blocks again), and exclusive ownership (a per-thread byte marker proves a held block is never written by another thread — no double-vend). The suite is gated behind PBR_MEMORY_POOL_THREAD_SAFETY != NONE (the default single-threaded build is intentionally racy per spec §2.4, so a placeholder runs there); since the library's thread-safety macro is PRIVATE (ADR-0020), the test CMake target mirrors the top-level cache variable so the gate sees the built mode, and links Threads::Threads. The cases run automatically under the M4.3 thread-safety CI job (MUTEX + LOCKFREE × GCC + Clang). A new tsan CI job (Clang + ThreadSanitizer) runs the suite under MUTEX, verifying the mutex-guarded path is data-race free. LOCKFREE is deliberately not run under TSan: a Treiber-stack pop reads a node's next-link another thread may have recycled — a benign race (the value is discarded when the tagged CAS fails; the pool backing is never unmapped, so the read is always of valid memory) that cannot be expressed as a well-defined atomic without C++20 std::atomic_ref / hazard pointers (out of scope); LOCKFREE concurrent correctness is covered by the logical invariants above plus the ADR-0020 §3 correctness argument, documented in the test header and the CI-job comment. No new ADR — design is ADR-0020/0021. Verified locally on MSVC 19.51: the stress suite passes under MUTEX and LOCKFREE, and the placeholder under NONE.
  • 4.5 Comparative benchmark: single-thread fast path vs. concurrent path (re-runs spec §6.3 in both modes). Implemented by extending pool_vs_malloc_bench (ADR-0014) with a concurrent scenario: T threads each run the interleaved alloc/free loop against a shared pool, started together via a release/acquire flag, reporting aggregate ns/op (wall-time ÷ total ops) vs malloc. New CLI: --threads N and --scenario {bulk|interleaved|concurrent|both|all}; the binary prints the thread_safety_policy it was built against (the bench target mirrors the library's PRIVATE PBR_MEMORY_POOL_THREAD_SAFETY macro) and clamps the concurrent scenario to one thread under the racy NONE build (spec §2.4) so it is safe in every configuration — that T=1 row is the fast-path baseline. Built once per policy, the canonical numbers are committed at docs/bench/v0.4.0-windows-msvc-x64-threading.md (Intel i5-6600K Skylake, MSVC 19.51 Release, 4 threads): the single-thread fast path is preserved (NONE interleaved ≈ 9 ns/op, matching M2.9, ~5× faster than malloc); synchronization has a real uncontended cost (MUTEX interleaved 47 ns/op, LOCKFREE 32 ns/op); under 4-thread contention LOCKFREE (41.8 ns/op) beats MUTEX (69.5 ns/op), but a single-shared-head pool cannot out-scale malloc's per-thread arenas — the evidence motivating the deferred per-thread caches (ADR-0020 §4). A bench-concurrent-smoke CI job builds + briefly runs the concurrent scenario under MUTEX and LOCKFREE (exit-code gate only, ADR-0014 §8). No new ADR — the methodology is ADR-0014; the policy set is ADR-0020. Spec Coverage Map §6.3 flips from 🚧 to ✅ (the concurrent comparative re-run completes the benchmark contract).
  • 4.6 Close Milestone 4 → v0.4.0: bump version.hpp, roll CHANGELOG.md, draft docs/releases/v0.4.0.md, open release PR (ADR-0004 §2). version.hpp is bumped to MINOR=4 PATCH=0 and PBR_MEMORY_POOL_VERSION_STRING to "0.4.0"; the pool_smoke version-check TEST_CASE asserts the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [0.4.0] — 2026-06-13 block (milestone headline + the M4.1–M4.5 Added subsections + a Spec Coverage Map note flipping §2.4 ⏳ → ✅ and confirming §6.3 🚧 → ✅), with the bottom-of-file link references rewritten. docs/releases/v0.4.0.md carries the human-prose release notes for release.yml. README.md status badge (green v0.3.0 C++ wrapper & type safety → green v0.4.0 thread-safe variant), status paragraph, and milestone table (Milestone 4 ⏳ next✅ complete, Milestone 5 ⏳ planned⏳ next) are refreshed. The maintainer reviews and merges this PR, then the agent tags v0.4.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced by release.yml.

Milestone 5 — Dynamic Growth Mode

Goal: optional behavior where the pool acquires additional contiguous chunks when exhausted (spec §2.2 — "request a new contiguous block if configured in dynamic mode").

  • 5.1 ADR: growth policy (geometric vs. linear) and chunk-linking strategy. Decided in ADR-0022: dynamic growth is opt-in, runtime, per-pool (default fixed — the v0.4.0 behaviour bit-for-bit), not a compile-time knob like ADR-0020's thread-safety, because growth's decision point is the exhaustion slow path (a pool that is not exhausted never evaluates the branch), so a runtime flag costs nothing in steady state and lets different pools choose differently. Growth is geometric (configurable factor F, default ×2: a new contiguous chunk supplies current_total × (F − 1) blocks, doubling the total) — linear is rejected because the decisive metric is chunk count: geometric keeps it O(log N), linear makes it O(N / chunk_size), which would degrade to O(N) the two new O(chunks) operations (the ADR-0012 foreign-pointer check on free, and destroy). Chunks are a singly-linked, append-only list of {backing, block_count, next} descriptors (the original pool is the first chunk) threaded by one shared implicit free list, so alloc (pop) and free-push stay O(1); only free's safety validation and destroy become O(chunks) = O(log N) in dynamic mode (fixed mode stays O(1) exactly as today). Per-chunk descriptors keep ADR-0015's per-block overhead at zero (descriptors are per-chunk, O(log N) total); the per-pool metadata grows (chunk-list head + growth config) and M5.3 must keep the ADR-0015 128-byte static_assert green or renegotiate. Chunks are never moved (address stability is the whole point of a pool — realloc/vector-style growth is hard-rejected). The chunk-list representation is the Composite, whose structure + implementation are M5.2; the C creation surface + growth slow path are M5.3. Seven rejected alternatives recorded (linear growth, compile-time knob, realloc/move, per-block chunk header, inline per-chunk descriptor [deferred to M5.2], per-chunk-exponential sizing variant, shrink-on-idle [out of scope]). No source changes in this PR.
  • 5.2 ADR + impl: Composite chunk-list representation linking the original pool with overflow chunks. Implemented per ADR-0023: memory_pool.cpp gains struct Chunk { void* backing_; std::size_t block_count_; Chunk* next_; } and a Chunk* overflow_ head on struct memory_pool. The Composite is inline-first-chunk: the root pool's existing backing_/block_count_ (ADR-0009 §6, preserved) are the first chunk; overflow chunks are forward-linked Chunk leaves. One shared implicit free list spans all chunks, so alloc (pop) and free-push stay O(1); only is_block_in_range (now walks the chunks via a shared block_in_chunk helper) and destroy (frees each chunk's backing + descriptor) become O(chunks) = O(log N) in dynamic mode — O(1) in fixed mode. memory_pool_metadata_bytes returns sizeof(memory_pool) + (overflow count) × sizeof(Chunk) (honest, O(chunks)); per-block overhead stays zero (descriptors are per-chunk, ADR-0022 §3). Why inline-first-chunk over a uniform list: measured, it keeps the MUTEX struct memory_pool at exactly 128 bytes (the ADR-0015 budget), so M5.2 needs no budget renegotiation, and keeps metadata_bytes honest (a uniform list would either under-report or, counting descriptors, bust the MUTEX budget at 136). M5.3's growth-config fields will renegotiate the budget per ADR-0015 §4. Behavior-preserving: overflow_ is always null until M5.3 (no growth trigger yet), so every operation reduces to its v0.4.0 single-backing path — verified by the full CTest suite passing unchanged in NONE, MUTEX, and LOCKFREE (7/7 each, MSVC 19.51), with the budget static_assert green in all three. Four rejected alternatives recorded (uniform chunk list, inline per-chunk descriptor, per-chunk free lists, raise-the-budget-now). The growth trigger + creation surface are M5.3.
  • 5.3 Implementation behind a runtime / compile-time flag; default remains fixed-size (spec §2.2). Implemented per ADR-0024 on the M5.2 Composite (ADR-0023). Growth is a runtime, per-pool flag — struct memory_pool carries one std::size_t grow_factor_ (0 = fixed default; ≥ 2 = grow geometrically by that factor) — set by the new ANSI-C memory_pool_create_dynamic(block_size, block_count, growth_factor) (and the C++ Pool::make_dynamic / PoolBuilder::with_growth_factor); the frozen spec §5 memory_pool_create stays fixed-mode. Growth runs inside pop_head (ADR-0021 §2) under the policy's existing synchronization: plain for SingleThreadedPolicy, under the held mutex for MutexPolicy. The grow_pool helper (compiled only for non-lock-free builds) computes the current total by walking the chunks, acquires a geometric overflow chunk of total × (factor − 1) blocks (ADR-0009 §3 overflow-guarded), initialises its slots into the now-empty shared free list, links it at the head of overflow_, and is noexcept — on std::bad_alloc it releases what it got and reports exhaustion (fixed-mode failure: NULL / std::bad_alloc). LOCKFREE + dynamic is rejected at creation (memory_pool_create_dynamic returns NULL, make_dynamicstd::nullopt): safe concurrent chunk-list growth needs atomic chunk links + a grow-lock (the free path walks the list and would race a concurrent append) and can't be TSan-verified — deferred, like per-thread caches (ADR-0024 §2); fixed-mode lock-free pools are fully supported. The ADR-0015 budget is renegotiated 128 → 192 per its §4 (the grow_factor_ field took the MUTEX struct to 136) — the compile-time static_assert and the pool_smoke runtime budget check move in lockstep; per-block overhead stays zero. c_consumer_min.c exercises memory_pool_create_dynamic under C89/C99. Two smoke TEST_CASEs (a dynamic pool grows 25× past its initial capacity then frees + destroys cleanly — leak-checked by the ASan cells; growth_factor < 2 rejected) are robust across all three builds (the grow case skips under the lock-free rejection). Verified locally: NONE / MUTEX / LOCKFREE all 7/7 (MSVC 19.51), budget static_assert green in each. Five rejected alternatives recorded in ADR-0024. Comprehensive exhaustion-and-grow tests + benchmarks are M5.4.
  • 5.4 Tests and benchmarks covering exhaustion-and-grow scenarios. Tests: a dedicated dynamic_growth CTest binary dynamic_growth_test.cpp whose target mirrors the library's PRIVATE thread-safety macro, so it knows whether it is a lock-free build. Under NONE / MUTEX it runs the full growth matrix — repeated geometric growth (1000 allocs from an initial 4 at factor 2, all distinct across chunks, metadata_bytes rising as overflow descriptors are added), several factors (2/3/4), full recovery / no-leak (drain → free → re-drain → destroy, ASan- and Valgrind-checked), the ADR-0012 range check spanning grown chunks (a foreign stack pointer is a no-op, a genuine grown-chunk block frees and re-issues LIFO), a fixed-pool exhaustion control, and the C++ Pool::make_dynamic / PoolBuilder::with_growth_factor surface. Under LOCKFREE it asserts the rejection contract (memory_pool_create_dynamic / make_dynamic / a growth PoolBuilder all fail — ADR-0024 §2) while fixed-mode pools still work. All three modes pass (MSVC 19.51). Benchmark: a new growth scenario in pool_vs_malloc_bench (--scenario growth|all) bulk-allocates into a dynamic pool that starts at 256 blocks and grows to iterations (skipped under lock-free); committed numbers at docs/bench/v0.5.0-windows-msvc-x64-growth.md — a growing pool is 1.96 × faster than malloc on amortized bulk alloc (55 ns/op vs 108), versus ~11 × for a pre-sized fixed pool, quantifying the geometric-growth overhead (the periodic O(log N) chunk acquisitions). The bench-concurrent-smoke CI job is broadened to bench-policy-smoke, running --scenario all (incl. growth) under MUTEX + LOCKFREE. No new ADR — design is ADR-0022/0023/0024.
  • 5.5 Close Milestone 5 → v0.5.0: bump version.hpp, roll CHANGELOG.md, draft docs/releases/v0.5.0.md, open release PR (ADR-0004 §2). version.hpp is bumped to MINOR=5 PATCH=0 and PBR_MEMORY_POOL_VERSION_STRING to "0.5.0"; the pool_smoke version-check TEST_CASE asserts the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [0.5.0] — 2026-06-13 block (milestone headline + the M5.1–M5.4 Added subsections + a Spec Coverage Map note flipping §2.2 🚧 → ✅), with the bottom-of-file link references rewritten. docs/releases/v0.5.0.md carries the human-prose release notes for release.yml. README.md status badge (green v0.4.0 thread-safe variant → green v0.5.0 dynamic growth), status paragraph, and milestone table (Milestone 5 ⏳ next✅ complete, Milestone 6 ⏳ planned⏳ next) are refreshed. The maintainer reviews and merges this PR, then the agent tags v0.5.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced by release.yml.

Milestone 6 — Observability & Decorators

Goal: optional logging / statistics / tracing without touching the hot path of release builds.

  • 6.1 ADR + impl: Decorator for an instrumented pool variant (counters, allocation histogram, optional logging). Implemented per ADR-0025 as the header-only instrumented_pool.hpp: InstrumentedPool composes a Pool (Decorator by composition — Pool is concrete + move-only with no virtual surface, so it wraps rather than inherits, like TypedPool / PoolAllocator) and re-exposes its allocation verbs, counting allocations_ / deallocations_ / allocation_failures_ and tracking live_ and its high-water mark peak_live_ (a relaxed compare-exchange max on each alloc). A copyable PoolStats snapshot via stats() and a write_summary(std::ostream&) provide on-demand logging; per-event lifecycle notification is the M6.2 Observer's job, not here. Counters are relaxed atomics, so the decorator is safe to wrap a thread-safe (MUTEX/LOCKFREE) pool and drive it concurrently (the live/peak high-water mark is an approximate diagnostic under contention); std::atomic is not movable, so a hand-written move ctor/assign loads and re-seeds the counters, keeping the type factory-returnable (make / make_dynamic mirror Pool). Zero overhead when disabled = opt-in by type — a program using Pool directly pays nothing (no counter, no branch, no atomic); M6.3 verifies the plain-Pool path is unchanged. The roadmap's "allocation histogram" is degenerate for a fixed-block pool (every block is block_size, one bucket), so peak_live_ is the occupancy signal instead — recorded in ADR-0025 §3. Six rejected alternatives recorded (GoF virtual-interface Decorator, template Decorator<PoolLike>, non-atomic counters, compile-time-gated instrumentation, per-op std::function logger, size histogram). A dedicated instrumented_pool CTest binary (five cases — counters/live/peak, exhaustion-failure counting + LIFO forwarding, write_summary, move semantics + pass-throughs, and an over-a-dynamic-pool case that never fails) passes under NONE / MUTEX / LOCKFREE (MSVC 19.51). Decorator flips to Implemented (row #10) in docs/patterns/README.md.
  • 6.2 ADR + impl: Observer for pool-lifecycle events (exhaustion, growth, destruction). Implemented per ADR-0026: the GoF runtime Observer — a PoolObserver abstract interface (virtual void on_pool_event(PoolEvent, const PoolStats&) noexcept) registered via InstrumentedPool::add_observer — is wired into the M6.1 Decorator's existing alloc/free interception points rather than a separate, non-stackable wrapper, so Decorator (stats) and Observer (events) compose in one observability type. Events: exhausted (try_allocatenullptr / allocatestd::bad_alloc), destroyed (the dtor notifies once; a moved-from instance has an emptied observer list so it notifies nobody), and grew — detected via a new O(1) core counter. Because growth happens inside the C pop_head (invisible above the C boundary, and diffing metadata_bytes per alloc would be O(chunks)), struct memory_pool gains std::atomic<std::size_t> grow_count_ incremented (relaxed) only in grow_pool (the rare slow path — hot path untouched), exposed by a new always-present, NULL-tolerant, C89-clean accessor size_t memory_pool_growths(const memory_pool_t*) (the InstrumentedPool reads it after each allocation in O(1) and notifies on a rise). on_pool_event is noexcept by contract (it may fire from the noexcept try_allocate and the dtor), so try_allocate keeps its noexcept. Zero overhead: plain Pool is unchanged; an observer-less InstrumentedPool does an empty-vector check + one relaxed load per alloc (M6.3 verifies). The grow_count_ field adds 8 bytes (NONE struct → 64, MUTEX → 144, within the 192 budget). Notification is not internally synchronized — observers must out-live the pool and be thread-safe for concurrent observable use (documented). c_consumer_min.c exercises memory_pool_growths under C89/C99; three new instrumented_pool TEST_CASEs cover exhaustion / destruction / growth notification, passing under NONE / MUTEX / LOCKFREE. Six rejected alternatives recorded in ADR-0026. Observer flips to Implemented (row #11) in docs/patterns/README.md. (clang-format + clang-tidy run locally before push — both clean.)
  • 6.3 Tests verifying zero-overhead in release builds when instrumentation is disabled. Implemented as the dedicated zero_overhead CTest binary zero_overhead_test.cpp, which discharges the ADR-0025 §5 contract ("instrumentation disabled" = opt-in by type: a program using Pool directly pays nothing). The two §5 obligations — the plain-Pool path is byte-identical, and the overhead lives only inside InstrumentedPool — are structural facts, so they are verified by static_assert (compile-time, config-independent — they hold in Release exactly as in Debug) plus a runtime behavioural-equivalence check, not a wall-clock benchmark: a timing gate on shared CI runners is too noisy to be meaningful (ADR-0014 §8) and a measured delta can never prove zero overhead the way the type structure does (pool_vs_malloc_bench remains the home for indicative numbers). Three structural proofs: (1) opt-in by type — a std::void_t detection idiom asserts the stats() / add_observer() surface is absent from Pool and present on InstrumentedPool, so a Pool holder cannot even name an instrumentation operation; (2) byte-identical footprintsizeof(Pool) == sizeof(memory_pool_t*) and Pool stays standard-layout (the decorator adds no member, vtable, or padding to it); (3) contained overheadsizeof(InstrumentedPool) >= sizeof(Pool) + 5 × sizeof(atomic<size_t>) + sizeof(size_t), so the counter cost is wholly inside the decorator. The runtime case proves a bare Pool and an InstrumentedPool over the same configuration are behaviourally indistinguishable: identical metadata_bytes() (the C struct memory_pool does not grow — instrumentation is header-only C++ state, ADR-0015), identical block_size(), identical capacity / exhaustion point, and the identical LIFO re-allocation signature (drain → free → re-drain yields the reverse of the first sequence on both paths, ADR-0009 §1). Four TEST_CASEs / 77 assertions; the binary is an ordinary CTest target, so it runs in every CI matrix cell including the Release cells — the "in release builds" coverage the item asks for, without a bespoke Release-only job. No new ADR — the methodology is fixed by ADR-0025 §5 (clang-format + clang-tidy run locally before push — both clean).
  • 6.4 Close Milestone 6 → v0.6.0: bump version.hpp, roll CHANGELOG.md, draft docs/releases/v0.6.0.md, open release PR (ADR-0004 §2). version.hpp is bumped to MINOR=6 PATCH=0 and PBR_MEMORY_POOL_VERSION_STRING to "0.6.0"; the pool_smoke version-check TEST_CASE asserts the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [0.6.0] — 2026-06-14 block (milestone headline + the M6.1–M6.3 Added subsections + a no-flip Spec Coverage Map note — observability is additive instrumentation, no spec row maps to it), with the bottom-of-file link references rewritten ([Unreleased]compare/v0.6.0...HEAD, new [0.6.0]releases/tag/v0.6.0). docs/releases/v0.6.0.md carries the human-prose release notes for release.yml. README.md status badge (green v0.5.0 dynamic growth → green v0.6.0 observability), the At a glance observability bullet, status paragraph, and milestone table (Milestone 6 ⏳ next✅ complete, Milestone 7 ⏳ planned⏳ next) are refreshed. The maintainer reviews and merges this PR, then the agent tags v0.6.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced by release.yml.

Milestone 7 — Release & Polish

Goal: ship a v1.0.0 reference implementation.

  • 7.1 Doxygen-generated API documentation published as a static site. Implemented per ADR-0027, closing the rendering half that ADR-0013 §4–§5 deferred to this item. A checked-in partial Doxyfile (docs/doxygen/Doxyfile) plus a hand-written landing page (docs/doxygen/mainpage.md) drive a dependency-free Doxygen HTML build — built-in theme with GENERATE_TREEVIEW, no Graphviz (HAVE_DOT = NO), no vendored CSS, no Python doc stack (the MkDocs/Sphinx-Breathe unified narrative site sketched in ADR-0013 §4 is explicitly deferred post-v1.0, consistent with spec §3.3). INPUT is the public-header contract surface only (*.h + *.hpp under src/main/cpp/it/d4np/memorypool/, EXTRACT_PRIVATE = NO), with PBR_MEMORY_POOL_DIAGNOSTICS=1 predefined so the ADR-0019 free-list iterator surface renders. The warn-as-error gate refines ADR-0013 §5: WARN_AS_ERROR = FAIL_ON_WARNINGS + WARN_IF_DOC_ERROR = YES fail CI on the doc-rot class (malformed commands, unresolved cross-references, stale @param names) while WARN_IF_UNDOCUMENTED / WARN_NO_PARAMDOC stay off — gating documentation correctness, not ceremonial exhaustiveness on every operator and trait typedef (ADR-0027 §3). PROJECT_NUMBER is left blank and injected at build time from version.hpp so the version string stays single-sourced. A new workflow (.github/workflows/docs-site.yml) builds the site as a warn-as-error gate on every PR (no deploy — PRs are never blocked by Pages configuration) and publishes to GitHub Pages on push to master via the official upload-pages-artifact / deploy-pages Actions (no generated HTML committed; build/ is git-ignored). Three public-header comments gain the Doxygen % auto-link escape (::%operator new / ::%operator delete) to silence spurious unresolved-link warnings on the global operators. Verified locally with Doxygen 1.10.0: zero warnings, build/doxygen/html/index.html generated. One-time maintainer action: enable GitHub Pages with Source: GitHub Actions in repository settings before the first master deploy (the PR gate works regardless).
  • 7.2 README: full usage example, performance summary, compatibility matrix. The README gains a Usage section with six compilable examples spanning the whole surface — the C four-function core, the RAII Pool (ctor / make / PoolBuilder, dual-verb allocate vs try_allocate), TypedPool<T> (construct / destroy), PoolAllocator<T> driving a std::list, dynamic growth via Pool::make_dynamic, and InstrumentedPool + a PoolObserver — each transcribed from a single program compiled, linked, and run against the real headers + memory_pool.cpp under MSVC 19.51 /W4 before commit (so no signature drift). The Performance section is consolidated into a one-paragraph summary plus three labelled regimes — fixed single-threaded (4–11×, M2.9), dynamic growth (~2×, M5.4), and threading (M4.5) — each linking its full docs/bench/ report, with the host disclosed once up front. A new Compatibility section captures the ADR-0005 contract: Tier-1 platforms × compiler floor versions, Tier-2 best-effort targets, the C++17 / C89+C99 language-standard matrix, the NONE/MUTEX/LOCKFREE thread-safety knob, and the zero-external-dependency guarantee. Documentation-only — no code, ADR, or spec change.
  • 7.3 CHANGELOG.md audit for the v1.0.0 entry: consolidate every Unreleased line accumulated since v0.6.0, verify category placement, and write the v1.0.0 summary headline (the file itself was introduced in Milestone 1.12). The [Unreleased] block accumulated since v0.6.0 (the M7.1 Added + Changed, the M7.2 Changed) is audited for Keep-a-Changelog category placement — the published Doxygen site and the docs-site workflow stay under Added (new capabilities), the header %-escapes and the README expansion under Changed (modifications to existing surfaces) — and a draft v1.0.0 summary headline is written: it frames v1.0.0 as the first stable release that freezes the public C ABI + C++ surface under the SemVer 1.0 promise, seals the M0–M6 feature set, and folds in the M7 release polish (M7.1 docs site, M7.2 README, M7.4 packaging, M7.5/M7.6 audits), with the row-by-row Spec Coverage Map acceptance deferred to M7.6. Per the established convention the per-task ### Added (MX.Y) / ### Changed (MX.Y) subsections are retained (not flattened — [0.6.0] keeps three ### Added (M6.x)); the block stays under ## [Unreleased] and is dated to ## [1.0.0] — <date> by the release PR (M7.7). Documentation-only — no code, ADR, or spec change; the bottom-of-file link references are untouched until M7.7 adds the [1.0.0] anchor.
  • 7.4 ADR: install / packaging layout (public-header export, pkg-config, CMake find_package config file) — phase 1 distribution per ADR-0004 §5. Implemented as ADR-0028 plus the CMake install/export machinery: a PBR_MEMORY_POOL_INSTALL option (default PROJECT_IS_TOP_LEVEL, so an embedding parent's cmake --install is not polluted) gates install(TARGETS … EXPORT) + install(EXPORT … NAMESPACE pbr::), the full it/d4np/memorypool/ public-header tree, a relocatable package config via configure_package_config_file with a SameMajorVersion version file, a pkg-config pbr-memory-pool.pc, and LICENSE under share/doc/. The internal target pbr_memory_pool carries EXPORT_NAME memory_pool so the installed imported target is pbr::memory_pool — identical to the in-build alias, so target_link_libraries(app pbr::memory_pool) is the one link line for add_subdirectory / FetchContent and installed-package consumers. release.yml's build-artifacts job now packages via cmake --install (full headers + archive + config + .pc) instead of hand-copying — fixing a latent bug where four of the seven public headers and the package config were missing from release tarballs (the ADR-0004 §5 "install the artifacts from a Release" path). Verified end-to-end locally (MSVC 19.51 + Ninja): configure → build → cmake --install to a staging prefix, then a separate consumer project did find_package(pbr_memory_pool CONFIG REQUIRED) + linked pbr::memory_pool and ran (consumer OK: got=7 block_size=64); the exported target resolves to pbr::memory_pool and the .pc carries the correct prefix/libdir/version. Phase 2 (vcpkg / Conan) stays deferred post-v1.0 (items 7.8 / 7.9). pkg-config .pc prefix= reflects the configure-time prefix (documented limitation, ADR-0028 §6).
  • 7.5 Patterns catalogue audit — verify every adopted pattern has both an ADR and a code location; refresh statuses. All eleven adopted patterns in docs/patterns/README.md (RAII, Pimpl, Factory Method, Builder, Adapter, Iterator, Strategy, Template Method, Composite, Decorator, Observer) were audited and confirmed to have (a) an Accepted ADR — 0010, 0011, 0018, 0019, 0020, 0021, 0023, 0025, 0026 — and (b) a live code location whose named symbol exists in src/main/cpp/it/d4np/memorypool/ (verified by grep: class Pool / PoolBuilder / PoolAllocator / FreeListIterator / FreeListView / InstrumentedPool, struct PoolObserver / Chunk, SingleThreadedPolicy / MutexPolicy / LockFreePolicy, alloc_skeleton / free_skeleton, the memory_pool_debug_* / memory_pool_growths accessors). Every status is correctly Implemented; no status changed. The audit refreshed forward-looking phrasing that diverged from the realized design: the Factory Method row no longer claims M4 thread-safety would be Factory-dispatched (it became the compile-time Strategy #7); the Builder row records that growth landed via .with_growth_factor() while thread safety is a compile-time knob (not a builder method); the Composite row drops the stale "dormant until M5.3" note (dynamic growth now populates overflow_); and the RAII row drops M2-era scaffolding phrasing. A dated audit-provenance note was added to the catalogue, and Object Pool is explicitly recorded as the project premise (spec-fixed, tracked under Candidate patterns) rather than a discretionary Adopted row. Documentation-only — no code, ADR, or spec change.
  • 7.6 Spec compliance acceptance — walk every row of the Spec Coverage Map (below) and confirm each requirement is satisfied by a passing test, a documented ADR, or both. Record the audit outcome in an ADR. Done as ADR-0029: every one of the fifteen Spec Coverage Map rows was re-verified end-to-end against live evidence — ten CTest targets (95 doctest cases across pool_smoke/typed_pool/pool_allocator/free_list_iterator/container_integration/concurrency_stress/dynamic_growth/instrumented_pool/zero_overhead + the spec_6_2_valgrind C program), the c_consumer_min.c ANSI-C consumer, and the ci.yml jobs (build incl. ASan/UBSan, ansi-c-compat C89/C99, zero-external-deps, valgrind, thread-safety, tsan, bench-smoke, bench-policy-smoke) — each cross-referenced to its satisfying ADR. Verdict: no gap, regression, or unsupported ✅ mark; the implementation satisfies every normative spec clause and the project is acceptance-ready for v1.0.0. No coverage cell changes state (all fifteen remain ✅); deferred items (double-free detection, per-thread caches, lock-free dynamic growth, shrink-on-idle) correspond to no spec clause and are out of acceptance scope. Documentation-only — no code or spec change.
  • 7.7 Close Milestone 7 → v1.0.0: bump version.hpp, roll CHANGELOG.md, draft docs/releases/v1.0.0.md, open the release PR for the maintainer to tag and publish (ADR-0004 §2). version.hpp is bumped to MAJOR=1 MINOR=0 PATCH=0 and PBR_MEMORY_POOL_VERSION_STRING to "1.0.0"; the pool_smoke version-check TEST_CASE asserts the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [1.0.0] — 2026-06-14 block (the M7.3 v1.0.0 headline, finalized, + the M7.1–M7.6 subsections), the meta-paragraph restored to a fresh empty [Unreleased], and the bottom-of-file link references rewritten ([Unreleased]compare/v1.0.0...HEAD, new [1.0.0]releases/tag/v1.0.0). docs/releases/v1.0.0.md carries the human-prose release notes for release.yml. README.md status badge (green v0.6.0 observability → bright-green v1.0.0 stable), status paragraph, and milestone table (Milestone 7 ⏳ next✅ complete, Milestone 8 ⏳ planned⏳ next) are refreshed. The maintainer reviews and merges this PR, then the agent tags v1.0.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced by release.yml.
  • 7.8 (Stretch, post-v1.0) vcpkg port: register pbr-memory-pool in microsoft/vcpkg, with portfile pinning to the v1.0.0 tag — phase 2 distribution per ADR-0004 §5. Implemented as ADR-0030 and an in-repo overlay port under ports/pbr-memory-pool/: vcpkg.json (name pbr-memory-pool, version 1.0.0, MIT, vcpkg-cmake + vcpkg-cmake-config host tools) and portfile.cmake, which vcpkg_from_github fetches the v${VERSION} source tag (SHA512-pinned to a559f0fb…97cdf), then builds from source through the project's own ADR-0028 install rules (tests/benchmarks off) and relocates the emitted find_package config (vcpkg_cmake_config_fixup PACKAGE_NAME pbr_memory_pool) and pkg-config .pc (vcpkg_fixup_pkgconfig) into vcpkg's layout — so a vcpkg consumer gets the same find_package(pbr_memory_pool CONFIG REQUIRED) + pbr::memory_pool as every other mode. Consumable today via vcpkg install pbr-memory-pool --overlay-ports=ports; VERSION is single-sourced from vcpkg.json (REF "v${VERSION}"), so a release bump touches only the manifest version + the SHA512. Upstream submission to microsoft/vcpkg is deferred (it is gated on registry-maintainer review and a per-release x-add-version obligation; the port is written to upstream conventions and the submission steps are documented in ports/README.md — ADR-0030). The port was not built through a live vcpkg install locally (vcpkg not provisioned), but follows the canonical helper pattern and its source SHA512 was verified by download; a vcpkg-CI smoke job is a candidate M8 follow-up. The sibling Conan recipe is §7.9.
  • 7.9 (Stretch, post-v1.0) Conan recipe: publish a conanfile.py to ConanCenter or a self-hosted recipe index, with the same v1.0.0 pin — phase 2 distribution per ADR-0004 §5. Implemented as ADR-0031 and a Conan 2.x recipe under conan/: conanfile.py + a ConanCenter-style test_package/. The recipe fetches the v<version> source tag (SHA256-pinned to 54e99b43…53c0), builds from source through the project's ADR-0028 CMake rules (tests/benchmarks off, fPIC honoured), then — because Conan's CMakeDeps generates the consumer-side config — drops the upstream-bundled lib/cmake + lib/pkgconfig + share and re-exposes the target through package_info (cmake_file_name = pbr_memory_pool, cmake_target_name = pbr::memory_pool), so a Conan consumer writes the identical find_package(pbr_memory_pool CONFIG REQUIRED) + pbr::memory_pool. Creatable today via conan create conan/ (which builds + runs the test_package); version single-sources the source URL, so a release bump touches only version + the sha256. Registry publication (ConanCenter / self-hosted) is deferred (gated on index-maintainer review + a per-release obligation; the recipe is written to ConanCenter conventions and the submission steps are documented in conan/README.md — ADR-0031). Mirrors the vcpkg port (§7.8 / ADR-0030): same build-from-source shape, same v1.0.0 pin, same deferral. The recipe was not built through a live conan create locally (Conan not provisioned), but its Python syntax is validated and the source SHA256 verified by download; a Conan-CI smoke job is a candidate M8 follow-up. Closes Milestone 7's stretch items — Milestone 7 is fully complete.

Milestone 8 — Internationalization & Post-Release Governance

Goal: post-v1.0.0, stand up a modular, professional documentation-translation system (Simplified Chinese + Japanese to start; English stays the single normative source), codify the maintenance protocol that governs updates / fixes / patches after a public release, and add an agent-runnable consistency lint that proves the project is still internally congruent after every change. This is additive, post-1.0 work → targets v1.1.0 (SemVer MINOR). Pre-1.0 closes at v1.0.0 (Milestone 7); Milestone 8 begins the maintained-product phase, so each item is sized to ship independently under the §6.1 one-PR-at-a-time rule.

  • 8.1 ADR: documentation i18n architecture — a modular per-language tree under docs/i18n/<lang>/ mirroring the translatable English surface, English as the single normative source, a machine-checkable translation-status manifest, a terminology glossary, and explicit English fallback for untranslated pages. File-based and zero-external-dependency (no SaaS translation platform in the build graph, consistent with spec §3.3's philosophy applied to docs). Language codes: zh-Hans (Simplified Chinese), ja (Japanese). The ADR fixes the translatable surface — what is localized (README, spec, getting-started / usage, patterns-catalogue overview) vs. English-only (ADRs, CHANGELOG.md, this ROADMAP.md): an ADR is an immutable architectural record, so localizing ADRs invites drift and is rejected. Decided in ADR-0032: translated pages live at docs/i18n/<lang>/<same-relative-path-as-the-English-source> (1:1 path mapping so the §8.6 lint can mechanically pair a translation with its source); English is normative and is the explicit fallback (no empty stubs — coverage is recorded in a per-language index + the manifest, each translated page carries a "source commit / English is normative" banner); the translatable surface is README + spec + getting-started/usage + patterns-catalogue overview prose, while ADRs / CHANGELOG.md / ROADMAP.md / AGENTS.md + workflow guides / the Doxygen API reference are English-only; a commit-pinned translation-status.md manifest (source path, source commit, translated-at commit, status, reviewer) makes staleness a CI-detectable fact for §8.6; and a glossary.md carries canonical ↔ zh-Hansja terms incl. explicit "keep in English" entries (free list, RAII, Pimpl, …). Six alternatives rejected (SaaS platform, localize-everything, gettext/.po, side-by-side bilingual files, branch-per-language, no-manifest). The scaffold/manifest/glossary themselves are §8.2; this item is the decision only — no source change.
  • 8.2 Scaffold the i18n skeleton: docs/i18n/README.md (contributor guide + translation workflow), the <lang>/ directory shape mirroring the §8.1 surface, docs/i18n/translation-status.md (per-file manifest: source path, source commit hash translated from, translated-at commit, status, reviewer), and docs/i18n/glossary.md (canonical term ↔ zh-Hansja, including explicit "keep in English" entries for free list, RAII, Pimpl, etc.). Implemented per ADR-0032: docs/i18n/README.md is the English contributor guide (translatable surface, mirrored-path layout, the per-page banner, the add/update-a-translation steps, the staleness contract); docs/i18n/translation-status.md seeds the manifest with all six rows (the three translatable pages — README.md, the spec, the patterns overview — × zh-Hans / ja) at missing, each pinning a source commit once translated; docs/i18n/glossary.md carries a Keep in English section (free list, RAII, Pimpl, O(1), the GoF pattern names, public identifiers, tool names) plus ~24 translatable terms with their zh-Hans / ja renderings. The <lang>/ directory shape is materialised by the per-language index pages zh-Hans/README.md and ja/README.md (localised title + intro + a status table linking each untranslated page to its English source — the explicit English fallback; no empty stubs per ADR-0032 §3). README's repository-layout table gains a docs/i18n/ row. Translations themselves are §8.3 (zh-Hans) / §8.4 (ja).
  • 8.3 Simplified Chinese (zh-Hans) translation of the §8.1 surface; each page seeded in the §8.2 manifest with the source commit hash it was translated from. Done page-by-page across three PRs, each a faithful zh-Hans translation at the 1:1 mirrored path with the "English is normative" banner and a commit-pinned manifest row: the spec (docs/i18n/zh-Hans/docs/specs/01_spec_cpp_memory_pool.md, source 2e55dfa), the patterns-catalogue overview (docs/i18n/zh-Hans/docs/patterns/README.md, source 524f0cc — the didactic overview per ADR-0032 §2, pointing to the English catalogue for the per-row ADR links), and the README (docs/i18n/zh-Hans/README.md — which doubles as the zh-Hans landing page, source a01d4f4). Code, identifiers, O(1), Free List, GoF pattern names, tool names, badge URLs, and numeric benchmark tables are kept verbatim per the glossary; relative links are recomputed for the deeper mirrored paths. All three zh-Hans manifest rows are translated. Japanese (ja) is §8.4.
  • 8.4 Japanese (ja) translation of the same surface; manifest entries seeded identically. Done page-by-page across three PRs, mirroring the M8.3 zh-Hans flow: the spec (docs/i18n/ja/docs/specs/01_spec_cpp_memory_pool.md, source 612f9d2), the patterns-catalogue overview (docs/i18n/ja/docs/patterns/README.md, source 6c6aeb7 — overview prose per ADR-0032 §2, pointing to the English catalogue for the per-row ADR links), and the README (docs/i18n/ja/README.md, the ja landing page, source be70cf8). Each is a faithful ja translation at the 1:1 mirrored path with the "English is normative" banner and a commit-pinned manifest row; code, identifiers, O(1), Free List, GoF pattern names, tool names, badge URLs, and numeric benchmark tables kept verbatim per the glossary. All three ja manifest rows are translated. With this, both target languages (zh-Hans, ja) cover the full §8.1 translatable surface.
  • 8.5 ADR + doc: post-release maintenance protocoldocs/workflow/maintenance.md defining the patch / minor / major decision tree (SemVer post-1.0), the hotfix-branch + backport workflow, the security-fix handling path, the deprecation policy, and exactly how version.hpp / CHANGELOG.md / release notes move for a patch release (v1.0.x) vs. a milestone minor. Formalizes the update / fix / patch governance for the maintained-product phase; cross-links ADR-0004 (versioning), ADR-0008 (tag delegation), and §11 of AGENTS.md. Done as ADR-0034 (the decision) + docs/workflow/maintenance.md (the governance doc). The protocol names the version-protected surface (C ABI + C++ types + compile-time knobs + the CMake imported target), fixes a three-question decision tree (break consumer code → MAJOR; backward-compatible addition / milestone / deprecation → MINOR; fix / docs / packaging / perf with no API change → PATCH; ambiguous rounds up), reuses the milestone-close mechanics (release.md) with only the moved version.hpp component differing per level, defines the hotfix path by releasability (fix on master → next PATCH when releasable; else branch from the released tag → PATCH → mandatory forward-port to master), a private-first, Security-categorized security path, and a deprecate-in-MINOR → window → remove-in-MAJOR deprecation policy. The agent-vs-human release boundary and tag delegation are unchanged. Five rejected alternatives recorded (ad-hoc per release, trunk-only without hotfix-from-tag, fold into release.md, heavyweight release-branch/LTS model, define-security-later). release.md gains a reciprocal pointer to maintenance.md. Documentation-only — no code change.
  • 8.6 Agent-runnable consistency lint + ADR: a dependency-free checker (portable runner under tools/, wired into a CI job — extends the existing docs.yml ADR-sanity checks) asserting the project is internally congruent after any change — version constants in lockstep across version.hpp / CHANGELOG.md / README badge / latest docs/releases/*.md; every ADR indexed and reciprocally linked; every catalogued pattern has both an ADR and a live code location; the Spec Coverage Map has no dangling rows; the i18n manifest (§8.2) has no entry staler than its English source; and ROADMAP.md checkbox state is internally consistent. Exits non-zero with an actionable report. The ADR records the check catalogue and the "post-release congruence" contract. Implemented as ADR-0035 + tools/consistency_lint.py (Python 3 standard-library only — dependency-free in the spec §3.3 sense applied to tooling; runnable as python tools/consistency_lint.py) + a new consistency job in docs.yml (full-history checkout for the git-based freshness check). The six checks: (1) version lockstep across version.hpp / CHANGELOG / README badge / newest release notes; (2) ADR index ↔ file bijection + sequential numbering; (3) every Adopted pattern row cites an existing ADR and an existing src/main/cpp/ path; (4) Spec Coverage Map has no dangling row (valid glyph + non-empty items cell); (5) no translated i18n manifest row is staler than its English source (git log <recorded>..HEAD -- <source> empty); (6) README milestone-complete state agrees with ROADMAP checkboxes + no malformed checkbox. It runs all checks then reports every failure and exits non-zero. Validated locally (passes on the consistent tree; a negative test confirmed it catches a deliberately-broken version badge). Five rejected alternatives recorded (pure-bash, no-lint, third-party framework, pre-commit-hook-only, symbol-level pattern check). Wiring the lint into the agent contract (pre-PR checklist) is §8.7.
  • 8.7 Wire the §8.6 lint into the agent contract: a post-release congruence checklist added to AGENTS.md (and surfaced as a checkbox in the PR template) — run the lint before drafting any post-1.0 PR — plus the mapping from each failing check to its remediation in the §8.5 maintenance protocol. Done: AGENTS.md §6.4 gains a mandatory post-release congruence check (run python tools/consistency_lint.py and make it pass before drafting any post-v1.0.0 PR, pointing at the failure→remediation map); the PR template gains a Documentation Impact checkbox for it; and docs/workflow/maintenance.md gains a failure → remediation table mapping each of the six lint checks (version-lockstep, adr-index, patterns, spec-map, i18n-freshness, milestones) to exactly how to fix it. CI still re-runs the lint (the consistency job, M8.6); this item makes "run it first, locally" a contract obligation. Documentation-only — no code change.
  • 8.8 Close Milestone 8 → v1.1.0: bump version.hpp (MINOR=1), roll CHANGELOG.md Unreleased into a [1.1.0] block, draft docs/releases/v1.1.0.md, refresh the README status block / badge / milestone table, and open the release PR (ADR-0004 §2). First post-1.0 minor — the §8.5 protocol governs from here on. version.hpp is bumped to MAJOR=1 MINOR=1 PATCH=0 and PBR_MEMORY_POOL_VERSION_STRING to "1.1.0"; the pool_smoke version-check TEST_CASE asserts the new components. CHANGELOG.md [Unreleased] is rolled into a sealed ## [1.1.0] — 2026-06-14 block (the M8.1–M8.7 + M8.9 subsections under a Milestone 8 headline; first post-1.0 MINOR, purely additive docs/governance — library binary unchanged); fresh empty [Unreleased]; bottom link references rewritten ([Unreleased]compare/v1.1.0...HEAD, new [1.1.0]releases/tag/v1.1.0). docs/releases/v1.1.0.md carries the human-prose release notes for release.yml. README.md status badge (v1.0.1v1.1.0), status paragraph, and milestone table (Milestone 8 ⏳ next✅ complete) are refreshed. The closing release classified as a MINOR per the M8.5 decision tree (additive surface, no API/ABI/behaviour change). The maintainer reviews and merges this PR, then the agent tags v1.1.0 from master per ADR-0008, and the maintainer clicks Publish on the draft GitHub Release. This closes Milestone 8 and the planned roadmap.
  • 8.9 (Emerged mid-M8, prerequisite for §8.3/§8.4) Establish English as the specification's normative language. The spec docs/specs/01_spec_cpp_memory_pool.md was authored in Italian (the original contract); ADR-0032 assumes a single English-normative source and AGENTS.md §2 mandates English for on-disk artifacts, so the spec cannot be coherently localized (the "English is normative" banner) while its source is Italian. Decided in ADR-0033: the spec is translated to English in place as the normative source — a faithful translation (every requirement, the API, the Free List description, the diagram, and the verification strategy preserved with identical meaning; only the prose language changes, so the frozen contract is not semantically altered), with the Italian original preserved in git history (commit 3ccff68). Three alternatives rejected (keep Italian + translate per-language, add it as an i18n target, dual-language file). This unblocks the §8.3 / §8.4 spec translations from a clean English source. documentation.md gains a "spec is maintained in English" note.

Spec Coverage Map

Traceability from the contract in docs/specs/01_spec_cpp_memory_pool.md to the roadmap items that fulfil it. Every spec requirement must terminate in at least one roadmap item; Milestone 7.6 (the acceptance audit before v1.0) walks this table row by row.

Legend: ⏳ pending · 🚧 in progress · ✅ done · ❎ not applicable (with reason).

Spec section Requirement Roadmap items Status
§2.1 Pre-allocate contiguous pool given block_size and block_count 1.6, 2.1, 2.3
§2.2 O(1) allocation; return NULL (C) or std::bad_alloc (C++); dynamic growth opt. 2.4, 3.1, 5.x
§2.3 O(1) deallocation; block marked free without returning to OS 2.4
§2.4 Optional, configurable thread safety; single-thread fast path preserved 4.1–4.5
§3.1 No memory leaks — destroy releases everything to the OS 2.3, 2.8
§3.2 Minimal metadata overhead per block 2.1, 2.10
§3.3 ANSI C / C++17 standard, no external dependencies 1.1, 1.10, 1.11
§4 Free List implicit (free blocks store the next-free pointer) 2.1, 2.3
§5 — create memory_pool_t* memory_pool_create(size_t block_size, size_t block_count) 1.6, 2.3
§5 — alloc void* memory_pool_alloc(memory_pool_t* pool) 1.6, 2.4
§5 — free void memory_pool_free(memory_pool_t* pool, void* block) 1.6, 2.4
§5 — destroy void memory_pool_destroy(memory_pool_t* pool) 1.6, 2.3
§6.1 Correctness — exhaustion, null inputs, foreign / out-of-range pointers 2.7
§6.2 Valgrind clean: ERROR SUMMARY: 0 errors from 0 contexts 2.8
§6.3 Benchmark pool_alloc/free vs malloc/free over 1,000,000 iterations 2.9, 4.5

When a roadmap item flips from ⏳ to ✅, update the corresponding cell(s) in this table in the same PR.

Acceptance (M7.6, 2026-06-14): all fifteen rows above were re-verified end-to-end against live tests, CI jobs, and ADRs and confirmed satisfied — the audit record is ADR-0029. The project is acceptance-ready for v1.0.0.


Session journal

The dated, end-of-session checkpoints — what got done, where the project stands, and how to resume — now live in docs/journal/, one file per session under YYYY/MM/, rather than inline in this file. Keeping them out of the roadmap leaves this document a forward-looking plan; the rationale and the agent rule are ADR-0036 and AGENTS.md §7.6.

Latest checkpoint: 2026-06-15 — Bug ledger & triage protocol — the in-repo bug ledger (docs/bugs/, ADR-0039) and the agent triage protocol (AGENTS.md §7.7) are now live; the planned roadmap (Milestones 0–8) remains complete and the project is in the maintained-product phase governed by docs/workflow/maintenance.md. The full trail is in docs/journal/.