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 ingit logis 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.
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.mdas the single source of truth for agent behavior. - 0.3 Add
CLAUDE.mdandGEMINI.mdadapters that defer toAGENTS.md. - 0.4 Scaffold
docs/withREADME.md,adr/,specs/, andworkflow/. - 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.mdwith numbered, checkbox-driven milestones (this file). - 0.9 Refresh
README.mdwith project description, status, and pointers toAGENTS.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) inAGENTS.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.
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.txtexposingsrc/main/cppas the public include root; declarepbr_memory_poollibrary target with sources globbed undersrc/main/cpp/it/d4np/memorypool/. - 1.3 Add
CMakePresets.jsonwithdebug,release,asan,ubsan,tsanpresets. - 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-tidywith the baseline check set declared inAGENTS.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), andversion.hpp(single source of truth for the project version constants consumed by CMake'sproject(... VERSION ...)) — no-op definitions, fully documented (spec §5; version constants per ADR-0004). Stub implementations live inmemory_pool.cppso 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 — gatemasteron all green. Implemented in.github/workflows/ci.ymlper ADR-0005 §4: Linux × {GCC, Clang} × {Debug, Release, ASan, UBSan} + Windows × MSVC × {Debug, Release} + macOS × Apple Clang × {Debug, Release, ASan, UBSan}, plusclang-formatrepo-wide check andclang-tidydiff 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.hand a minimal C TU under-std=c89 -pedantic -Werrorand-std=c99 -pedantic -Werrorto enforce the C interop contract (spec §3.3). The minimal C consumer lives atsrc/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 withPBR_MEMORY_POOL_BUILD_TESTS=OFFso doctest is not fetched, then asserts nofind_packagein the library scope and inspects the resultinglibpbr_memory_pool.aarchive for stray third-party objects. - 1.12 Add the initial
CHANGELOG.mdat the repo root in Keep a Changelog 1.1.0 format. TheUnreleasedsection 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.ymltriggered onv*tag push: re-run the full test matrix, build per-platform binaries (Linux x86_64, Windows x86_64, macOS arm64 — degrade gracefully where unavailable), emitSHA256SUMS, and create a draft GitHub Release with the correspondingdocs/releases/v<X.Y.Z>.mdas the body (ADR-0004 §4). Implemented via three jobs —verify(workflow_call intoci.ymlfor defense-in-depth re-verification of the tagged commit),build-artifacts(per-platform tar.gz packaging the static library + public headers + LICENSE + README + CHANGELOG), andrelease(download every artifact, emitSHA256SUMS, draft the GitHub Release with the body fromdocs/releases/<tag>.md). Pre-release suffixes (-alpha.N/-beta.N/-rc.N) are auto-detected and propagated to the GitHub Release.workflow_dispatchis 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: bumpversion.hppto0.1.0, rollCHANGELOG.mdUnreleased into a[0.1.0]block with ISO date, adddocs/releases/v0.1.0.mdrelease notes, open the release PR for the maintainer to tag and publish (seedocs/workflow/release.md). The version constants were set to0.1.0pre-emptively in Milestone 1.6 (under the assumption that M1 would close atv0.1.0), so the "bump" step is a no-op verification rather than a real edit; theCHANGELOG.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), anddocs/releases/v0.1.0.mdis added with human-prose release notes grouped by theme. The maintainer tagsv0.1.0frommasterafter this PR merges and clicks Publish on the draft GitHub Release produced byrelease.yml. - 1.15 CMake configure-smoke CI workflow (
.github/workflows/build-smoke.yml) — early subset of §1.8. Runscmake --preset debugand--preset releaseon every PR touching CMake / sources / configs. Catches latentCMakeLists.txtand preset breakage (like the version-regex zero bug fixed in PR #6) before it reaches a fresh-clone consumer. Superseded by §1.8 —build-smoke.ymlis removed in the same PR that introducesci.yml; the full build matrix subsumes the smoke configure step.
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_sizeminimum (≥sizeof(void*)), and alignment guarantee (spec §4, spec §2.1). Implemented as ADR-0009: the free list is implicit (next-pointer in firstsizeof(void*)bytes of free slots) and initialised in ascending address order;block_sizeis strictly validated againstblock_size ≥ sizeof(void*)ANDblock_size % alignof(std::max_align_t) == 0(no silent rounding —NULLon violation);block_count > 0with a mandatorysize_toverflow guard onblock_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 isalignof(std::max_align_t)(drop-inmallocparity). Thestruct memory_poolfield 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++Poolis a move-only RAII owner ofmemory_pool_t*(ctor →memory_pool_create, dtor →memory_pool_destroy, copy deleted, singlememory_pool_t* handle_data member,sizeof(Pool) == sizeof(void*));struct memory_poolis forward-declared inmemory_pool.hand defined exclusively inmemory_pool.cpp(C-style Pimpl — the C handle is the Impl, no separatePool::Implstruct). Both patterns are co-introduced and interdependent, so they share a single ADR per AGENTS.md §8 #5. Catalogue updated: two new rows indocs/patterns/README.mdAdopted / Planned, statusPlanneduntil M2.3 lands the body ofstruct memory_pooland the meaningful semantics of the C functions. Five rejected alternatives recorded (classical C++ Pimpl withunique_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_createandmemory_pool_destroywith contiguous backing allocation (spec §2.1, spec §5, spec §3.1 — destroy releases all pre-allocated memory). Bodies land inmemory_pool.cppreplacing the M1 stubs:struct memory_poolis defined (Pimpl per ADR-0010 — the C handle is the Impl), the three ADR-0009 §2block_sizepreconditions and theblock_count > 0+size_t-overflow guard from §3 are enforced (NULLon violation, never silent rounding), the contiguous backing is obtained via::operator new(total, std::align_val_t{alignof(std::max_align_t)})withstd::bad_alloccaught at the boundary, and the implicit free list is initialised in ascending address order usingstd::memcpyfor strict-aliasing safety.memory_pool_destroyreleases the backing through the matching aligned::operator deleteand then frees the metadata struct;NULLis a no-op.memory_pool_alloc/memory_pool_freeremain M1 stubs — their O(1) bodies arrive in M2.4. Companion edits:memory_pool.handmemory_pool.hppDoxygen spell out the three preconditions + thealignof(max_align_t)return-pointer alignment guarantee linking ADR-0009;pool_smoke_test.cppis refactored with seven newTEST_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 fromPlannedtoImplemented. - 2.4 Implement
memory_pool_allocandmemory_pool_freeagainst the implicit free list — both O(1);allocreturnsNULLon exhaustion in fixed mode (spec §2.2, spec §2.3, spec §5). Bodies inmemory_pool.cppreplace the remaining M1 stubs:memory_pool_allocis a constant-time pop of the implicit free-list head (block = pool->head_; pool->head_ = *static_cast<void**>(block); return block;with explicitnullptrreturns on null pool and on exhausted pool);memory_pool_freeis 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.cppgains six newTEST_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::PoolRAII wrapper. The wrapper's bodies (ctor →memory_pool_create, dtor →memory_pool_destroy, move-construct / move-assign,allocate/deallocate/native_handleforwarders) were co-introduced with the C-side implementation in M2.3 and M2.4 — seememory_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.hppDoxygen 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*), singlememory_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'smake, per AGENTS.md §8 #5).static std::optional<Pool> Pool::make(block_size, block_count)is the Factory Method — engaged optional on success,std::nullopton any ADR-0009 §2/§3 failure, orthogonal to the M3.1std::bad_alloc-throwing decision.class PoolBuilderis the Builder — fluent.with_block_size().with_block_count().build()returningstd::optional<Pool>viaPool::make; constbuild()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 (statusImplemented). Seven newTEST_CASEs inpool_smoke_test.cppcover the happy path, three failure paths (misaligned, zero count, default-constructed builder, partially-configured builder), and the multi-build property of the constbuild(). 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-0012 —memory_pool_freeruns anO(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 usesstd::uintptr_tarithmetic to avoid[expr.rel]/4unspecified behaviour on cross-allocation pointer<. The C++Pool::deallocatewrapper inherits the policy through its forward tomemory_pool_free. Five newTEST_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 exactgcc -g -O0 ... && valgrind --leak-check=full --show-leak-kinds=all ./test_poolinvocation from spec §6.2 as a literal demonstrative test undersrc/test/cpp/it/d4np/memorypool/spec_6_2_valgrind/so the spec-named verification path is reproducible 1:1. Implemented in.github/workflows/ci.ymlas thevalgrindjob on Ubuntu 24.04: installs Valgrind viaapt, compilestest_pool.cundergcc -std=c89 -pedantic -g -O0andmemory_pool.cppunderg++ -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 insrc/test/cpp/it/d4np/memorypool/spec_6_2_valgrind/README.md), links withg++, then runsvalgrind --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=definite,indirect --error-exitcode=1and additionally greps the output for the literal spec success criterion. The--errors-for-leak-kinds=definite,indirectflag is the only non-spec addition — without it, a leaked backing buffer would pass--error-exitcodewhile violating spec §3.1;still reachableandpossiblestay informational so global libstdc++ state does not trip the gate. Thetest_pool.cbinary 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 beforememory_pool_destroyreleases the backing. - 2.9 Microbenchmark vs
malloc/freeover 1,000,000 iterations undersrc/bench/cpp/it/d4np/memorypool/(spec §6.3); numbers committed and summarised in the README. Implemented aspool_vs_malloc_benchper ADR-0014: hand-rolledstd::chrono::steady_clocktiming 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 headlinemalloc_median / pool_medianratio per region. Per-iterationvolatilebyte write + a portabledo_not_optimizebarrier defeat Release-mode dead-code elimination on both allocators identically. The binary is built off by default; the newbenchpreset (Release +PBR_MEMORY_POOL_BUILD_BENCHMARKS=ON+PBR_MEMORY_POOL_BUILD_TESTS=OFF) opts in. Abench-smokeCI 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 atdocs/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 functionsize_t memory_pool_metadata_bytes(const memory_pool_t*)added tomemory_pool.h; the C++ side gains the[[nodiscard]] std::size_t Pool::metadata_bytes() const noexceptforwarder inmemory_pool.hpp. Per-pool metadata issizeof(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 viastatic_assert(sizeof(memory_pool) <= 128U, ...)inmemory_pool.cpp(fires on every cell of the 14-cell build matrix) and at runtime via four newTEST_CASEs inpool_smoke_test.cppcovering 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 newmemory_pool_metadata_bytesdeclaration is held to the same ANSI C C89 compatibility contract as the rest of the public C API and is exercised byc_consumer_min.cunder the M1.10-std=c89 -pedantic -Werrorand-std=c99 -pedantic -WerrorCI jobs. Spec Coverage Map §3.2 flips from ⏳ to ✅. - 2.11 Close Milestone 2 →
v0.2.0: bumpversion.hpp, rollCHANGELOG.md, draftdocs/releases/v0.2.0.md, open release PR (ADR-0004 §2).version.hppis bumped toMAJOR=0 MINOR=2 PATCH=0and thePBR_MEMORY_POOL_VERSION_STRINGconstant to"0.2.0"in lockstep; thepool_smokeversion-checkTEST_CASEis updated to assert the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [0.2.0] — 2026-06-11block 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.mdcarries the human-prose release notes thatrelease.ymlconsumes as the GitHub Release body.README.mdstatus badge, status paragraph, and milestone table are refreshed (badge from yellowv0.1.0 build skeletonto greenv0.2.0 single-thread MVP; Milestone 2 row from⏳ nextto✅ complete; Milestone 3 row from⏳ plannedto⏳ next). The maintainer reviews and merges this PR, then the agent tagsv0.2.0frommasterper ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced byrelease.yml.
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 —
NULLon the C side,std::bad_allocon the C++ side, behind a configurable knob (spec §2.2 — "returnNULL(or throw an exception in C++)"). Implemented as ADR-0016: the C ABI is exception-free forever (every C failure isNULL/ 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()throwsstd::bad_allocon exhaustion (and on a moved-from wrapper), the newtry_allocate()isnoexceptand returnsnullptrwith the exact v0.2.0 semantics. ThePoolctor now throwsstd::bad_allocon construction failure (amends ADR-0010 §2's silent empty state — recorded on its Status line);Pool::make/PoolBuilder::buildremain the non-throwing path, restructured around a private adopt-handle ctor so no try/catch is involved. The benchmark's timed loops switch totry_allocate()(apples-to-apples withmalloc's in-bandNULL; 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_thrownaming inversion,std::invalid_argumentsplit,std::expectedclone, runtime per-pool flag). Five new / updatedTEST_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 andtry_allocate/allocatevariants. Implemented per ADR-0017 as a header-only template intyped_pool.hppcomposing the untypedPool: the spec-conformantblock_sizeis derived at compile time (max(sizeof(T), sizeof(void*))rounded up to thealignof(std::max_align_t)multiple) so every ADR-0009 §2 precondition holds by construction, and over-alignedTis rejected with astatic_assertinstead of silently receiving under-aligned storage. The surface is two-layer (ADR-0017 §3): typed storage verbsallocate/try_allocate/deallocatefollow the ADR-0016 dual-verb policy verbatim, and the object-lifetime pairconstruct(Args&&...)/destroyplacement-news / destroys aTwith the strong exception guarantee on a throwingTctor (the slot returns to the free list before the exception propagates). RAII / move-only lifetime, the throwing ctor, and the non-throwingTypedPool::makeFactory Method are inherited structurally fromPoolthrough composition. Eight newTEST_CASEs in a dedicatedtyped_pool_testCTest binary cover compile-time block-size properties, dual-verb exhaustion, the fullconstruct/destroylifecycle 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 fromPool, rawsizeof(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-blockvoid*interface to the variable-sizestd::allocator_traitscontract. It is a non-owning back-reference to aPool(singlePool*member,sizeof == sizeof(void*); the pool must out-live every container and adapter copy, thestd::pmr::polymorphic_allocatorlifetime contract). Routing (ADR-0018 §2): a request routes to the pool iffn == 1 && sizeof(T) <= pool.block_size() && alignof(T) <= alignof(std::max_align_t)— served byPool::allocate(O(1),std::bad_allocon exhaustion per ADR-0016 §2); everything else (n > 1, oversized / over-alignedT, rebound nodes larger than the block) delegates to over-aligned::operator new/::operator deletewith asize_t-overflow guard. Because the standard guaranteesdeallocate(p, n)receives the samen/type as the matchingallocate, andblock_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. Sostd::list/std::map/std::setrun entirely on the pool fast path whilestd::vectorruns 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, andpropagate_on_container_swapare allstd::false_type;is_always_equalisstd::false_type(stateful);operator==is true iff the two adapters reference the samePool;select_on_container_copy_constructionkeeps the default (copies retain the back-reference). A new introspection accessorsize_t memory_pool_block_size(const memory_pool_t*)(ADR-0018 §3 — theO(1),NULL-tolerant companion tomemory_pool_metadata_bytes, held to the same ANSI-C C89 contract and exercised byc_consumer_min.c) backs the size-fit decision, with a[[nodiscard]] std::size_t Pool::block_size() const noexceptforwarder. Seven newTEST_CASEs in a dedicatedpool_allocatorCTest binary cover pool-fast-path exhaustion, multi-block + oversized-Tfallback (pool left untouched), equality / statefulness / rebinding, the propagation-traitstatic_asserts, and end-to-endstd::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_resourcesubclass — 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:FreeListIteratoris a LegacyForwardIterator walking the implicit free list (ADR-0009 §1) —value_typeisconst 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;FreeListViewis the range adaptor (begin()/end(), constructible from aconst memory_pool_t*or aPool&) enabling range-for,std::distance,std::find, etc. Gating (disabled in release unless explicitly enabled): the entire diagnostic surface sits behind thePBR_MEMORY_POOL_DIAGNOSTICSmacro, which defaults to1when!NDEBUG(debug) and0whenNDEBUG(release), any explicit definition winning; the CMake optionPBR_MEMORY_POOL_ENABLE_DIAGNOSTICS(defaultOFF) is the documented opt-in, forcing the macro to1as 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 insidememory_pool.cpp, honouring the ADR-0010 Pimpl boundary instead of re-encoding ADR-0009 §1 in a public header;nextreads the link via astatic_cast<void* const*>that provably casts away noconst. The accessors are exercised byc_consumer_min.cunder the M1.10 C89/C99 jobs (guarded by the same macro). A dedicatedfree_list_iteratorCTest binary carries five cases when diagnostics are on (ascending strided walk of a fresh pool withfree_countvsstd::distancecross-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 proxyoperator*). - 3.5 Tests against
std::vector,std::list, and a small custom container. Implemented as the dedicatedcontainer_integrationCTest binarycontainer_integration_test.cpp, driving the M3.3PoolAllocator<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 inmemory_pool_debug_free_countacross pushes andclear, a measurement robust to any implementation-defined sentinel node), exercised with bothintandstd::stringelements (the latter validating non-trivialconstruct/destroyunder ASan/Valgrind);std::vector(contiguousn > 1→ heap fallback; checked for contents, growth past the pool's capacity, copy viaselect_on_container_copy_construction, and<algorithm>interop, not pool occupancy); and a small custom containerForwardList<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 throughstd::allocator_traits, proving the adapter works with an arbitrary conforming container, not just the standard ones. The custom container is driven with bothstd::allocator(generic, pool-agnostic) andPoolAllocator(with the guarded free-count assertion and a live-instanceTrackedtype provingdestroyruns on every element). SevenTEST_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 needsblock_size ≥ sizeof(rebound node)(always larger thansizeof(T)), so pick a generousblock_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: bumpversion.hpp, rollCHANGELOG.md, draftdocs/releases/v0.3.0.md, open release PR (ADR-0004 §2).version.hppis bumped toMINOR=3 PATCH=0andPBR_MEMORY_POOL_VERSION_STRINGto"0.3.0"in lockstep; thepool_smokeversion-checkTEST_CASEis updated to assert the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [0.3.0] — 2026-06-13block (milestone headline + the M3.1–M3.5Added/Changedsubsections + 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.mdcarries the human-prose release notes thatrelease.ymlconsumes as the GitHub Release body.README.mdstatus badge (greenv0.2.0 single-thread MVP→ greenv0.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 tagsv0.3.0frommasterper ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced byrelease.yml.
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_SAFETYmacro (default…_NONE, plus…_MUTEXand…_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(astd::mutexacross 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:MutexPolicygrowsstruct memory_pool(≈ 80 bytes for a Windowsstd::mutex) and must keep the ADR-0015static_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_freeare refactored into thealloc_skeleton/free_skeletonTemplate Method templates inmemory_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 insidepop_head, not the skeleton, so the futureLockFreePolicycan 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 onlySingleThreadedPolicy(the v0.3.0 head pop/push verbatim, no synchronization) and hard-wires it viausing 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 addsMutexPolicy/LockFreePolicybesideSingleThreadedPolicyand turnsActivePolicyinto aPBR_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.cppnow defines all three policies, compiling exactly the one selected byPBR_MEMORY_POOL_THREAD_SAFETY(a#if … #elif … #else #errorblock aliasing it toActivePolicy) —SingleThreadedPolicy(NONE, default, the v0.3.0 head pop/push verbatim),MutexPolicy(MUTEX, astd::mutexheld across the O(1) pop/push; the ADR-0012 foreign-pointer guard runs in the skeleton outside the lock), andLockFreePolicy(LOCKFREE, a Treiber-stackcompare_exchange_weakloop on an ABA-taggedstd::atomic<TaggedHead>head — in-slot next-links stay plain pointers, only the head is atomic, the tag defeats ABA, and the 3-argument CAS givesacq_relsuccess / derived-acquirefailure).struct memory_poolgains the policy's state conditionally — a 16-bytestd::atomic<TaggedHead>head under LOCKFREE, astd::mutex mutex_under MUTEX — and the ADR-0015static_assert(sizeof(memory_pool) <= 128)stays green in all three modes (verified: MSVCstd::mutexlands the MUTEX struct at ~120 bytes, within budget). The macro constants + NONE default live inmemory_pool.h(NOLINTBEGIN/END forcppcoreguidelines-macro-usage, C89-clean); the CMake optionPBR_MEMORY_POOL_THREAD_SAFETY(NONE|MUTEX|LOCKFREE) maps to a PRIVATE compile definition on the library (it changes onlymemory_pool.cpp's internal policy, not the public ABI).createseeds the head per policy; the diagnostic accessors load the atomic head under LOCKFREE. A newthread-safetyCI 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_stressCTest binaryconcurrency_stress_test.cpp, which drivesPoolfromTHREAD_COUNT(8) threads to validate theMutexPolicy/LockFreePolicyimplementations (ADR-0020) on three invariants: no over-vend / distinctness (a concurrent drain hands out exactlyblock_countblocks, every one distinct), full recovery / no leak (after 8 × 20 000 alloc/free churn the pool vends exactlyblock_countdistinct 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 behindPBR_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 linksThreads::Threads. The cases run automatically under the M4.3thread-safetyCI job (MUTEX + LOCKFREE × GCC + Clang). A newtsanCI 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++20std::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:Tthreads each run the interleaved alloc/free loop against a shared pool, started together via a release/acquire flag, reporting aggregatens/op(wall-time ÷ total ops) vsmalloc. New CLI:--threads Nand--scenario {bulk|interleaved|concurrent|both|all}; the binary prints thethread_safety_policyit was built against (the bench target mirrors the library's PRIVATEPBR_MEMORY_POOL_THREAD_SAFETYmacro) and clamps the concurrent scenario to one thread under the racyNONEbuild (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 atdocs/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 (NONEinterleaved ≈ 9 ns/op, matching M2.9, ~5× faster thanmalloc); synchronization has a real uncontended cost (MUTEXinterleaved 47 ns/op,LOCKFREE32 ns/op); under 4-thread contentionLOCKFREE(41.8 ns/op) beatsMUTEX(69.5 ns/op), but a single-shared-head pool cannot out-scalemalloc's per-thread arenas — the evidence motivating the deferred per-thread caches (ADR-0020 §4). Abench-concurrent-smokeCI 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: bumpversion.hpp, rollCHANGELOG.md, draftdocs/releases/v0.4.0.md, open release PR (ADR-0004 §2).version.hppis bumped toMINOR=4 PATCH=0andPBR_MEMORY_POOL_VERSION_STRINGto"0.4.0"; thepool_smokeversion-checkTEST_CASEasserts the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [0.4.0] — 2026-06-13block (milestone headline + the M4.1–M4.5Addedsubsections + 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.mdcarries the human-prose release notes forrelease.yml.README.mdstatus badge (greenv0.3.0 C++ wrapper & type safety→ greenv0.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 tagsv0.4.0frommasterper ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced byrelease.yml.
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 suppliescurrent_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 onfree, anddestroy). 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, soalloc(pop) andfree-push stay O(1); onlyfree's safety validation anddestroybecome 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-bytestatic_assertgreen 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.cppgainsstruct Chunk { void* backing_; std::size_t block_count_; Chunk* next_; }and aChunk* overflow_head onstruct memory_pool. The Composite is inline-first-chunk: the root pool's existingbacking_/block_count_(ADR-0009 §6, preserved) are the first chunk; overflow chunks are forward-linkedChunkleaves. One shared implicit free list spans all chunks, soalloc(pop) andfree-push stay O(1); onlyis_block_in_range(now walks the chunks via a sharedblock_in_chunkhelper) anddestroy(frees each chunk's backing + descriptor) become O(chunks) = O(log N) in dynamic mode — O(1) in fixed mode.memory_pool_metadata_bytesreturnssizeof(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 MUTEXstruct memory_poolat exactly 128 bytes (the ADR-0015 budget), so M5.2 needs no budget renegotiation, and keepsmetadata_byteshonest (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 budgetstatic_assertgreen 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_poolcarries onestd::size_t grow_factor_(0 = fixed default; ≥ 2 = grow geometrically by that factor) — set by the new ANSI-Cmemory_pool_create_dynamic(block_size, block_count, growth_factor)(and the C++Pool::make_dynamic/PoolBuilder::with_growth_factor); the frozen spec §5memory_pool_createstays fixed-mode. Growth runs insidepop_head(ADR-0021 §2) under the policy's existing synchronization: plain forSingleThreadedPolicy, under the held mutex forMutexPolicy. Thegrow_poolhelper (compiled only for non-lock-free builds) computes the current total by walking the chunks, acquires a geometric overflow chunk oftotal × (factor − 1)blocks (ADR-0009 §3 overflow-guarded), initialises its slots into the now-empty shared free list, links it at the head ofoverflow_, and isnoexcept— onstd::bad_allocit releases what it got and reports exhaustion (fixed-mode failure:NULL/std::bad_alloc).LOCKFREE+ dynamic is rejected at creation (memory_pool_create_dynamicreturnsNULL,make_dynamic→std::nullopt): safe concurrent chunk-list growth needs atomic chunk links + a grow-lock (thefreepath 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 (thegrow_factor_field took the MUTEX struct to 136) — the compile-timestatic_assertand thepool_smokeruntime budget check move in lockstep; per-block overhead stays zero.c_consumer_min.cexercisesmemory_pool_create_dynamicunder C89/C99. Two smokeTEST_CASEs (a dynamic pool grows 25× past its initial capacity then frees + destroys cleanly — leak-checked by the ASan cells;growth_factor < 2rejected) 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), budgetstatic_assertgreen 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_growthCTest binarydynamic_growth_test.cppwhose 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_bytesrising 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_factorsurface. Under LOCKFREE it asserts the rejection contract (memory_pool_create_dynamic/make_dynamic/ a growthPoolBuilderall fail — ADR-0024 §2) while fixed-mode pools still work. All three modes pass (MSVC 19.51). Benchmark: a newgrowthscenario inpool_vs_malloc_bench(--scenario growth|all) bulk-allocates into a dynamic pool that starts at 256 blocks and grows toiterations(skipped under lock-free); committed numbers atdocs/bench/v0.5.0-windows-msvc-x64-growth.md— a growing pool is 1.96 × faster thanmallocon 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). Thebench-concurrent-smokeCI job is broadened tobench-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: bumpversion.hpp, rollCHANGELOG.md, draftdocs/releases/v0.5.0.md, open release PR (ADR-0004 §2).version.hppis bumped toMINOR=5 PATCH=0andPBR_MEMORY_POOL_VERSION_STRINGto"0.5.0"; thepool_smokeversion-checkTEST_CASEasserts the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [0.5.0] — 2026-06-13block (milestone headline + the M5.1–M5.4Addedsubsections + a Spec Coverage Map note flipping §2.2 🚧 → ✅), with the bottom-of-file link references rewritten.docs/releases/v0.5.0.mdcarries the human-prose release notes forrelease.yml.README.mdstatus badge (greenv0.4.0 thread-safe variant→ greenv0.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 tagsv0.5.0frommasterper ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced byrelease.yml.
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:InstrumentedPoolcomposes aPool(Decorator by composition —Poolis concrete + move-only with no virtual surface, so it wraps rather than inherits, likeTypedPool/PoolAllocator) and re-exposes its allocation verbs, countingallocations_/deallocations_/allocation_failures_and trackinglive_and its high-water markpeak_live_(a relaxed compare-exchange max on each alloc). A copyablePoolStatssnapshot viastats()and awrite_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::atomicis not movable, so a hand-written move ctor/assign loads and re-seeds the counters, keeping the type factory-returnable (make/make_dynamicmirrorPool). Zero overhead when disabled = opt-in by type — a program usingPooldirectly pays nothing (no counter, no branch, no atomic); M6.3 verifies the plain-Poolpath is unchanged. The roadmap's "allocation histogram" is degenerate for a fixed-block pool (every block isblock_size, one bucket), sopeak_live_is the occupancy signal instead — recorded in ADR-0025 §3. Six rejected alternatives recorded (GoF virtual-interface Decorator, templateDecorator<PoolLike>, non-atomic counters, compile-time-gated instrumentation, per-opstd::functionlogger, size histogram). A dedicatedinstrumented_poolCTest 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 toImplemented(row #10) indocs/patterns/README.md. - 6.2 ADR + impl: Observer for pool-lifecycle events (exhaustion, growth, destruction). Implemented per ADR-0026: the GoF runtime Observer — a
PoolObserverabstract interface (virtual void on_pool_event(PoolEvent, const PoolStats&) noexcept) registered viaInstrumentedPool::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_allocate→nullptr/allocate→std::bad_alloc),destroyed(the dtor notifies once; a moved-from instance has an emptied observer list so it notifies nobody), andgrew— detected via a new O(1) core counter. Because growth happens inside the Cpop_head(invisible above the C boundary, and diffingmetadata_bytesper alloc would be O(chunks)),struct memory_poolgainsstd::atomic<std::size_t> grow_count_incremented (relaxed) only ingrow_pool(the rare slow path — hot path untouched), exposed by a new always-present, NULL-tolerant, C89-clean accessorsize_t memory_pool_growths(const memory_pool_t*)(theInstrumentedPoolreads it after each allocation in O(1) and notifies on a rise).on_pool_eventisnoexceptby contract (it may fire from thenoexcepttry_allocateand the dtor), sotry_allocatekeeps itsnoexcept. Zero overhead: plainPoolis unchanged; an observer-lessInstrumentedPooldoes an empty-vector check + one relaxed load per alloc (M6.3 verifies). Thegrow_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.cexercisesmemory_pool_growthsunder C89/C99; three newinstrumented_poolTEST_CASEs cover exhaustion / destruction / growth notification, passing under NONE / MUTEX / LOCKFREE. Six rejected alternatives recorded in ADR-0026. Observer flips toImplemented(row #11) indocs/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_overheadCTest binaryzero_overhead_test.cpp, which discharges the ADR-0025 §5 contract ("instrumentation disabled" = opt-in by type: a program usingPooldirectly pays nothing). The two §5 obligations — the plain-Poolpath is byte-identical, and the overhead lives only insideInstrumentedPool— are structural facts, so they are verified bystatic_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_benchremains the home for indicative numbers). Three structural proofs: (1) opt-in by type — astd::void_tdetection idiom asserts thestats()/add_observer()surface is absent fromPooland present onInstrumentedPool, so aPoolholder cannot even name an instrumentation operation; (2) byte-identical footprint —sizeof(Pool) == sizeof(memory_pool_t*)andPoolstays standard-layout (the decorator adds no member, vtable, or padding to it); (3) contained overhead —sizeof(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 barePooland anInstrumentedPoolover the same configuration are behaviourally indistinguishable: identicalmetadata_bytes()(the Cstruct memory_pooldoes not grow — instrumentation is header-only C++ state, ADR-0015), identicalblock_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). FourTEST_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: bumpversion.hpp, rollCHANGELOG.md, draftdocs/releases/v0.6.0.md, open release PR (ADR-0004 §2).version.hppis bumped toMINOR=6 PATCH=0andPBR_MEMORY_POOL_VERSION_STRINGto"0.6.0"; thepool_smokeversion-checkTEST_CASEasserts the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [0.6.0] — 2026-06-14block (milestone headline + the M6.1–M6.3Addedsubsections + 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.mdcarries the human-prose release notes forrelease.yml.README.mdstatus badge (greenv0.5.0 dynamic growth→ greenv0.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 tagsv0.6.0frommasterper ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced byrelease.yml.
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 withGENERATE_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).INPUTis the public-header contract surface only (*.h+*.hppundersrc/main/cpp/it/d4np/memorypool/,EXTRACT_PRIVATE = NO), withPBR_MEMORY_POOL_DIAGNOSTICS=1predefined 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 = YESfail CI on the doc-rot class (malformed commands, unresolved cross-references, stale@paramnames) whileWARN_IF_UNDOCUMENTED/WARN_NO_PARAMDOCstay off — gating documentation correctness, not ceremonial exhaustiveness on every operator and trait typedef (ADR-0027 §3).PROJECT_NUMBERis left blank and injected at build time fromversion.hppso 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 tomastervia the officialupload-pages-artifact/deploy-pagesActions (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.htmlgenerated. One-time maintainer action: enable GitHub Pages with Source: GitHub Actions in repository settings before the firstmasterdeploy (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-verballocatevstry_allocate),TypedPool<T>(construct/destroy),PoolAllocator<T>driving astd::list, dynamic growth viaPool::make_dynamic, andInstrumentedPool+ aPoolObserver— each transcribed from a single program compiled, linked, and run against the real headers +memory_pool.cppunder MSVC 19.51/W4before 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 fulldocs/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, theNONE/MUTEX/LOCKFREEthread-safety knob, and the zero-external-dependency guarantee. Documentation-only — no code, ADR, or spec change. - 7.3
CHANGELOG.mdaudit for the v1.0.0 entry: consolidate every Unreleased line accumulated sincev0.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 sincev0.6.0(the M7.1Added+Changed, the M7.2Changed) is audited for Keep-a-Changelog category placement — the published Doxygen site and thedocs-siteworkflow stay under Added (new capabilities), the header%-escapes and the README expansion under Changed (modifications to existing surfaces) — and a draftv1.0.0summary headline is written: it framesv1.0.0as 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_packageconfig file) — phase 1 distribution per ADR-0004 §5. Implemented as ADR-0028 plus the CMake install/export machinery: aPBR_MEMORY_POOL_INSTALLoption (defaultPROJECT_IS_TOP_LEVEL, so an embedding parent'scmake --installis not polluted) gatesinstall(TARGETS … EXPORT)+install(EXPORT … NAMESPACE pbr::), the fullit/d4np/memorypool/public-header tree, a relocatable package config viaconfigure_package_config_filewith aSameMajorVersionversion file, a pkg-configpbr-memory-pool.pc, andLICENSEundershare/doc/. The internal targetpbr_memory_poolcarriesEXPORT_NAME memory_poolso the installed imported target ispbr::memory_pool— identical to the in-build alias, sotarget_link_libraries(app pbr::memory_pool)is the one link line foradd_subdirectory/FetchContentand installed-package consumers.release.yml'sbuild-artifactsjob now packages viacmake --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 --installto a staging prefix, then a separate consumer project didfind_package(pbr_memory_pool CONFIG REQUIRED)+ linkedpbr::memory_pooland ran (consumer OK: got=7 block_size=64); the exported target resolves topbr::memory_pooland the.pccarries the correct prefix/libdir/version. Phase 2 (vcpkg / Conan) stays deferred post-v1.0 (items 7.8 / 7.9). pkg-config.pcprefix=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) anAcceptedADR — 0010, 0011, 0018, 0019, 0020, 0021, 0023, 0025, 0026 — and (b) a live code location whose named symbol exists insrc/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, thememory_pool_debug_*/memory_pool_growthsaccessors). Every status is correctlyImplemented; 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 populatesoverflow_); 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+ thespec_6_2_valgrindC program), thec_consumer_min.cANSI-C consumer, and theci.ymljobs (buildincl. ASan/UBSan,ansi-c-compatC89/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 forv1.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: bumpversion.hpp, rollCHANGELOG.md, draftdocs/releases/v1.0.0.md, open the release PR for the maintainer to tag and publish (ADR-0004 §2).version.hppis bumped toMAJOR=1 MINOR=0 PATCH=0andPBR_MEMORY_POOL_VERSION_STRINGto"1.0.0"; thepool_smokeversion-checkTEST_CASEasserts the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [1.0.0] — 2026-06-14block (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.mdcarries the human-prose release notes forrelease.yml.README.mdstatus badge (greenv0.6.0 observability→ bright-greenv1.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 tagsv1.0.0frommasterper ADR-0008, and the maintainer clicks Publish on the draft GitHub Release produced byrelease.yml. - 7.8 (Stretch, post-v1.0) vcpkg port: register
pbr-memory-poolin 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 underports/pbr-memory-pool/:vcpkg.json(namepbr-memory-pool, version1.0.0, MIT,vcpkg-cmake+vcpkg-cmake-confighost tools) andportfile.cmake, whichvcpkg_from_githubfetches thev${VERSION}source tag (SHA512-pinned toa559f0fb…97cdf), then builds from source through the project's own ADR-0028 install rules (tests/benchmarks off) and relocates the emittedfind_packageconfig (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 samefind_package(pbr_memory_pool CONFIG REQUIRED)+pbr::memory_poolas every other mode. Consumable today viavcpkg install pbr-memory-pool --overlay-ports=ports;VERSIONis single-sourced fromvcpkg.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-releasex-add-versionobligation; the port is written to upstream conventions and the submission steps are documented inports/README.md— ADR-0030). The port was not built through a livevcpkg installlocally (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.pyto 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 underconan/:conanfile.py+ a ConanCenter-styletest_package/. The recipe fetches thev<version>source tag (SHA256-pinned to54e99b43…53c0), builds from source through the project's ADR-0028 CMake rules (tests/benchmarks off,fPIChonoured), then — because Conan'sCMakeDepsgenerates the consumer-side config — drops the upstream-bundledlib/cmake+lib/pkgconfig+shareand re-exposes the target throughpackage_info(cmake_file_name = pbr_memory_pool,cmake_target_name = pbr::memory_pool), so a Conan consumer writes the identicalfind_package(pbr_memory_pool CONFIG REQUIRED)+pbr::memory_pool. Creatable today viaconan create conan/(which builds + runs thetest_package);versionsingle-sources the source URL, so a release bump touches onlyversion+ thesha256. 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 inconan/README.md— ADR-0031). Mirrors the vcpkg port (§7.8 / ADR-0030): same build-from-source shape, samev1.0.0pin, same deferral. The recipe was not built through a liveconan createlocally (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.
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, thisROADMAP.md): an ADR is an immutable architectural record, so localizing ADRs invites drift and is rejected. Decided in ADR-0032: translated pages live atdocs/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-pinnedtranslation-status.mdmanifest (source path, source commit, translated-at commit, status, reviewer) makes staleness a CI-detectable fact for §8.6; and aglossary.mdcarries canonical ↔zh-Hans↔jaterms 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), anddocs/i18n/glossary.md(canonical term ↔zh-Hans↔ja, including explicit "keep in English" entries forfree list,RAII,Pimpl, etc.). Implemented per ADR-0032:docs/i18n/README.mdis 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.mdseeds the manifest with all six rows (the three translatable pages —README.md, the spec, the patterns overview — ×zh-Hans/ja) atmissing, each pinning a source commit once translated;docs/i18n/glossary.mdcarries a Keep in English section (free list,RAII,Pimpl,O(1), the GoF pattern names, public identifiers, tool names) plus ~24 translatable terms with theirzh-Hans/jarenderings. The<lang>/directory shape is materialised by the per-language index pageszh-Hans/README.mdandja/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 adocs/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 faithfulzh-Hanstranslation 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, source2e55dfa), the patterns-catalogue overview (docs/i18n/zh-Hans/docs/patterns/README.md, source524f0cc— 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 thezh-Hanslanding page, sourcea01d4f4). 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 threezh-Hansmanifest rows aretranslated. 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.3zh-Hansflow: the spec (docs/i18n/ja/docs/specs/01_spec_cpp_memory_pool.md, source612f9d2), the patterns-catalogue overview (docs/i18n/ja/docs/patterns/README.md, source6c6aeb7— 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, thejalanding page, sourcebe70cf8). Each is a faithfuljatranslation 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 threejamanifest rows aretranslated. With this, both target languages (zh-Hans,ja) cover the full §8.1 translatable surface. - 8.5 ADR + doc: post-release maintenance protocol —
docs/workflow/maintenance.mddefining 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 howversion.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 ofAGENTS.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 movedversion.hppcomponent differing per level, defines the hotfix path by releasability (fix onmaster→ next PATCH when releasable; else branch from the released tag → PATCH → mandatory forward-port tomaster), 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 intorelease.md, heavyweight release-branch/LTS model, define-security-later).release.mdgains a reciprocal pointer tomaintenance.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 existingdocs.ymlADR-sanity checks) asserting the project is internally congruent after any change — version constants in lockstep acrossversion.hpp/CHANGELOG.md/ README badge / latestdocs/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; andROADMAP.mdcheckbox 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 aspython tools/consistency_lint.py) + a newconsistencyjob indocs.yml(full-history checkout for the git-based freshness check). The six checks: (1) version lockstep acrossversion.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 existingsrc/main/cpp/path; (4) Spec Coverage Map has no dangling row (valid glyph + non-empty items cell); (5) notranslatedi18n 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 (runpython tools/consistency_lint.pyand make it pass before drafting any post-v1.0.0PR, pointing at the failure→remediation map); the PR template gains a Documentation Impact checkbox for it; anddocs/workflow/maintenance.mdgains 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 (theconsistencyjob, 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: bumpversion.hpp(MINOR=1), rollCHANGELOG.mdUnreleasedinto a[1.1.0]block, draftdocs/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.hppis bumped toMAJOR=1 MINOR=1 PATCH=0andPBR_MEMORY_POOL_VERSION_STRINGto"1.1.0"; thepool_smokeversion-checkTEST_CASEasserts the new components.CHANGELOG.md[Unreleased]is rolled into a sealed## [1.1.0] — 2026-06-14block (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.mdcarries the human-prose release notes forrelease.yml.README.mdstatus badge (v1.0.1→v1.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 tagsv1.1.0frommasterper 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.mdwas authored in Italian (the original contract); ADR-0032 assumes a single English-normative source andAGENTS.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 (commit3ccff68). Three alternatives rejected (keep Italian + translate per-language, additas an i18n target, dual-language file). This unblocks the §8.3 / §8.4 spec translations from a clean English source.documentation.mdgains a "spec is maintained in English" note.
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.
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/.