Retrospective: M6b (hardening for 0.2.0)#
Status: complete on the branch m6b-hardening (from main at 7d64c90, the merge of pull
request #8 with M7). M6b is the hardening half of PLAN 11.3’s M6, which the author’s re-grouping
of 2026-10-02 (PLAN Appendix F, GOVERNANCE.md) moved from a 0.1.x release into 0.2.0; the
other half, M6a (the CUDA plugin wheels), ran at the same time on m6a-cuda-wheels. Three steps,
each with a section below; then the review and its fixes, the milestone’s acceptance table, the
verification from a fresh clone, the lessons, the state of 0.2.0 from M6b’s side, the re-estimate
and the open items. The milestone’s two ADRs were written as 0030 and 0032 and renumbered 0033 and
0034 in the review (M6a uses 0030-0032); the sections below use the new numbers.
Step 1: the tutorial algorithms (tutorials)#
What was built#
Commits |
What |
|---|---|
|
|
|
|
|
the framework operators’ executors ( |
|
|
|
the conformance kit generates undirected graphs for an algorithm that requires them; C2, C7 and C9 count each stored direction; C10 merges the two algorithms’ requirements |
|
|
|
|
|
the tutorial “Your first dynamic algorithm” ( |
Results#
ctest --preset dev -L 'conformance|triangle_delta|dynamic_bfs': 283/283;ctest --preset dev-cudawith the same labels on GPU 1: 543/543 (C8 budgets and C12 streams on CUDA included; C4 skipped, each tutorial has one engine).compute-sanitizer memcheck with leak checks on both tutorial CUDA conformance executables: 0 errors, 0 leaks. The
asanpreset on the two tutorials: 192/192.Every algorithm paired with
triangle_deltain C10 (sssp, cycle_count, mosp, dynamic_bfs) now also passes on undirected graphs; the directed models and their random streams are unchanged.The tutorial was followed in a fresh clone: the scaffold is green (40/40), the step-4 edits as written are green (40/40), the “break it on purpose” step fails C2, C5, C8 and the hand test, the documented seed replay reproduces C2, and
--removeleaves the tree clean.Two manual mutations (no ownership rule in
triangle_delta; no subtree walk indynamic_bfs) are caught by the kit.
Deviations and decisions (with reasons)#
All recorded in ADR 0033 (accepted under delegation):
Registered algorithms, not examples. PLAN 9.3 put the tutorial algorithms in
examples/tutorial_algorithms/, unregistered and not installed. The M6 criteria (created withnew_algorithm.py, maturitytutorial, the full kit on every backend) need registered algorithms, so they live incpp/src/algorithmswith installed headers (in the API baseline);examples/tutorial_algorithms/holds the tutorial’s reference solution.maturity_level::tutorialis a public addition, appended to the frozen enumeration.Undirected graphs in the kit, which the triangle count needs.
No citation for the tutorial algorithms (
test_api_surface.pyaccepts an emptycitefor maturitytutorial).No CLI command, no
dyng.update()support and noexamples/cppprogram for them (ADR 0033,python_gaps.md): teaching material, not research algorithms.The tutorial’s
my_bfsis simpler thandynamic_bfs(sequential hooks, no BFS tree); the tutorial says whatdynamic_bfsadds.
Step 2: the documentation on GitHub Pages, fuzzers, mutation suites, property runs (docs-pages-fuzz)#
What was built#
Commits |
What |
|---|---|
|
|
|
six libFuzzer targets ( |
|
|
|
the command line’s text batches reject bad input as |
|
the site on GitHub Pages: |
|
|
Results#
The mutation run at
a99e968(about 27 minutes): the control passed, 11/11 mutations detected.Fuzzing after the fixes: every target 5 and then 15 minutes locally without a finding (0.47M to 5.6M inputs per target);
ctest --preset fuzz6/6;ci/fuzz.sh --time 15end to end.The three reader findings fail on the old code (3.2 GB, 4 GB and 1.6 GB allocation requests) and pass now.
The Hypothesis properties of the tutorials (2,500 examples, 82 s) and the robustness tests (5,000 examples) pass.
A fresh clone at
ed497dcpassedci/check.sh --parity; actionlint, zizmor andgithub_meta_check.py --verify-pinsclean.
Deviations and decisions (with reasons)#
All recorded in ADR 0034 (accepted under delegation):
cpp/fuzz/, not the rootfuzz/of the PLAN 4.2 sketch.No binary-reader target: dynG has no binary reader yet (
.dgbmoved to M8; MOSP’s binary graph cache is write-only); they come with M8 (0.3.0).The command line’s text batches are parsed in Python, out of libFuzzer’s reach; a Hypothesis robustness test covers them.
The mosp mutations were chosen here (PLAN 8.4 lists none): they undo MOSP-OpenMP’s combined-graph fix M-d.
ADR number: written as 0032, because both branches had written an ADR 0030 and 0031 was left for the renumbering; M6a then wrote its own 0032 too, so the review renumbered M6b’s ADRs to 0033 (the tutorial algorithms) and 0034 (this one).
The preset
fuzzbuilds only the sssp module, static, Debug with-O1.The CSR-triplet findings came from a review prompted by the fuzzer’s memory use, not from a crash; the documentation says so.
Step 3: sanitizer CI and close-out (sanitizers-finish)#
What was built#
Commits |
What |
|---|---|
|
the cycle_count kernel tests detect Clang’s sanitizers ( |
|
|
|
|
|
|
|
|
|
the CHANGELOG ( |
Results#
The sanitizer presets on the development machine (ctest -L cpu -j 8, 2026-10-06):
Preset |
Compiler |
Tests |
Test time |
Reports |
|---|---|---|---|---|
|
GCC 12 |
826 passed |
101 s |
none |
|
GCC 12 |
733 passed |
141 s |
none |
|
Clang 18.1.8 + libomp 18 (conda |
826 passed |
134 s |
none |
Archer was checked both ways: without
OMP_TOOL_LIBRARIESthetsan-openmptests report races inside libomp (__kmp_lock_suspend_mx,__kmpc_dispatch_next_8); with it, andignore_noninstrumented_modules=1, a small OpenMP program with a real race (a shared sum next to a reduction) is still reported, and the same program without the race is clean.actionlint, zizmor,
github_meta_check.py --verify-pinsand shellcheck: clean.
Deviations and decisions (with reasons)#
A third sanitizer job,
tsan-openmp. The criterion names the existingasanandtsanpresets.tsanturns OpenMP off (GCC’s libgomp is not instrumented, M1a), so it checks only thestd::threadcode, and every OpenMP backend would stay unchecked for races. Clang’s libomp with Archer makes TSan understand OpenMP; the new preset and job cover the OpenMP backends of every algorithm. Both TSan jobs run:tsankeeps thestd::asyncpath ofutil/concurrent.hpp(taken only without OpenMP) and GCC’s TSan.TSan + OpenMP + GCC is a configure error, not a silent configuration whose every test fails.
vm.mmap_rnd_bits=28in the hosted jobs. Theubuntu-24.04runners randomize mmap with 32 bits, which the TSan and ASan runtimes of GCC 13 and LLVM 18 do not support (a known runner-image issue); the step is in the workflow only.Not a required check yet (the criterion):
repository_settings.mdstep 9 names the three check names for when the lead maintainer requires them.The re-grouping beyond the two named pages. The criterion names
docs/roadmap.mdanddocs/developer/plan.md; the documentation, the design sketches, the docstrings and four messages also named planned versions (the hypergraph “0.2”, vertex batches “0.3”, cycle modes “0.4”, the public framework “0.5”), so they move with it. The texts about the CUDA plugins (“0.1.x”) are M6a’s and were left alone. The Python tutorials and the MOSP tutorial, planned for 0.1.x and not written, are now listed for a 0.2.x release (docs/tutorials/index.md), not for 0.2.0.ADR 0029 stays Proposed (superseded in the review, which applied the author’s decision of 2026-10-06:
6fab3d9). The milestone’s criterion 5 says so. The orchestration notes (pending_author_facts.md, 2026-10-06) record that the author chose option A on 2026-10-06; applying it (ADR 0029 Accepted, an approvals-log row, the open-decisions table) is left to the orchestrator, who scoped this step (open item 1).No ADR for the sanitizer jobs: they carry out a planned item (PLAN 7.3, 8.8) without changing a rule; this record and
robustness.mdhold the decisions.
Review and fixes#
Independent reviewers (lenses: the tutorials for a newcomer, robustness, the docs site) found 22 problems, each confirmed by a second reviewer; all are fixed on this branch (two pairs reported the same problem under two lenses and share a row):
# |
Finding (severity) |
Fix |
Commits |
|---|---|---|---|
1 |
|
the seed offers only along an edge of G_{t+1} (it looks for v in u’s out-row, as |
|
2 |
The kit never applied the insertions first and never generated a batch that both inserts and deletes one edge; |
a third property preset ( |
|
3 |
Tutorial step 4.2 added a second |
the step says to replace the scaffold’s definition; |
|
4 |
|
the line is gone; |
|
5 |
The reference solution kept the placeholder’s comments (“so update() recomputes”, a TODO) (low) |
the comments describe the incremental algorithm; section 4.7 tells the reader which doc comments to reword |
|
6 |
The tutorial presented C3 as a check of the OpenMP backend, and did not explain the skipped tests (low) |
4.9: C3 is trivially green until |
|
7, 17 |
After the tutorial’s clean-up the build tree kept |
section 6 configures again with |
|
8, 19 |
The tutorial implied that the conda environment provides a compiler and gave other minimum versions than the install page (low) |
step 1 says a system compiler is needed with or without conda and points to the install page’s table; the install page no longer says the compiler comes “from the dyng-dev environment” |
|
9 |
The fuzzers’ round trip ran inside |
|
|
10 |
|
the step, as in |
|
11 |
The third reader finding had no reproducer, and libFuzzer’s default 2 GB limit could not catch it (low) |
|
|
12 |
|
at most 1000 s per target; the upload also runs when the job is cancelled |
|
13 |
The Pages site (built from |
a banner on every page while |
|
14 |
ADR numbers collided with M6a’s twice, and the merge note was wrong (high) |
M6b’s ADRs are 0033 and 0034, with every reference; the merge note below is corrected |
|
15 |
The roadmap promised Python tutorials (and a DOI) in 0.2.0 that no milestone delivers (medium) |
moved to 0.2.x, as the tutorials index says; the DOI depends on checkpoint A4 |
|
16 |
|
|
|
18 |
ADR 0029 was shown as Proposed although the author decided on 2026-10-06 (medium) |
Accepted (2026-10-06, by the author, option A): the ADR, the index, an approvals-log row (and out of the open decisions), the pages, header and test that called it proposed |
|
20 |
|
the default comes from |
|
21 |
User pages named milestones (M1a, M7) and plan sections, and sssp’s page still said the MOSP totals “are gated when mosp is ported” (low) |
the maturity lines name releases (“new in 0.2.0, not released yet”); the sentence points to mosp’s gate section; the plan references are gone from the algorithm, API and tutorial pages; |
|
22 |
The release checklist named Read the Docs and no Pages check; |
|
|
Not fully done, with the reason:
Plan references outside the algorithm, API and tutorial pages. The contributor how-tos (
how_to/add_an_algorithm.md,how_to/port_research_code.md) and the design sketches (docs/design/sketches/) still cite PLAN sections: they are written for contributors, who are pointed to the plan’s summary indeveloper/plan.md; rewording them is left to the next documentation pass.docs/getting_started/install.md,docs/getting_started/index.mdanddocs/api/python/index.mdstill say the CUDA plugins come in “0.1.x” on this branch: M6a rewrites those passages, so M6b leaves them to the merge (item 1 below) instead of creating a conflict.ci/tutorial_edits.pyencodes the prose of section 4; a change of that prose needs the matching change of the script (its docstring says so), so the check proves the prose builds as the script reads it, not that a human reads it the same way. The tutorial was also followed by hand-written edits in a fresh clone (below).
Results#
The minimal case of finding 1 fails before the fix on every backend (sequential, OpenMP, CUDA:
DynamicBfs.AnEdgeInsertedThenDeletedOffersNoLevel) and passes after it.ctest -R 'C[0-9]+_|DynamicBfs|TriangleDelta'indev-cuda(GPU 1): 541/541 with the new preset and mix (every algorithm, host and CUDA executables).ci/fuzz.sh --time 30(Clang 18):ctest --preset fuzz6/6, every target 30 s without a finding;ctest -L fuzzindev: 14/14 (six replays, five self-tests, three regressions).ci/scaffold_check.sh: the probes 80/80 and the tutorial’smy_bfsmade byci/tutorial_edits.py41/41.The tutorial followed in a fresh clone at
ef594cd(step 3: 40/40 with C4 and C12 skipped per graph type; section 4’s edits: 41/41; “break it on purpose”: C2, C5, C8 and the hand test fail, with the documented seed line; the clean-up: a configure that still namesmy_bfsfails with the list of algorithms,-DDYNG_ALGORITHMS=allconfigures every algorithm, the tree is clean).
Milestone acceptance#
# |
Criterion |
Evidence |
Status |
|---|---|---|---|
1 |
Tutorial algorithms |
step 1; ADR 0033; the verification below (the kit on every backend in |
met |
2 |
Docs site on GitHub Pages: |
step 2; Pages enabled by the author on 2026-10-06 (step 3 records it); the |
met |
3 |
libFuzzer targets for every text reader and the binary readers (Clang, CMake option / preset), a hosted job, any finding fixed with a regression test; |
step 2 (ADR 0034): six targets, three findings fixed; 11/11 mutations detected; profile |
met (binary readers with M8) |
4 |
Sanitizer CI: hosted ASan + UBSan and TSan jobs on pull requests (CPU tests), green, not required; compute-sanitizer stays in |
step 3: |
met locally |
5 |
The author’s decisions of 2026-10-02 in |
step 3 ( |
met |
6 |
A fresh clone passes |
“Verification” below |
met |
Verification after the review fixes (fresh clones of m6b-hardening)#
Two clones of the local repository at ef594cd (git checkout m6b-hardening) in
$DYNG_SCRATCH/review, run at the same time, every heavy step under the shared perf lock:
Check |
Result |
|---|---|
|
all checks passed in 26 min: clang-format; |
|
all steps passed in 66 min: |
After |
|
|
|
Portability pre-checks (working tree after the C++ changes) |
the Clang 18 syntax pass over a |
|
|
Verification before the review (fresh clone of m6b-hardening)#
All runs in $DYNG_SCRATCH/runs/m6b-san/clone (a clone of the local repository, git checkout m6b-hardening), every heavy step under the shared perf lock, while other agents’ builds ran:
Check |
Result |
|---|---|
|
24 min: clang-format; |
|
|
|
all steps passed in 62 min (RTX A5000, driver 590.48.01, CUDA 13.1; started at |
|
at |
Portability pre-checks (on the working tree at |
the Clang 18 syntax pass over a |
Lessons#
Each new check found something the old ones could not. The fuzzers found three readers that trusted a size field (one only through the review their memory use prompted); the Hypothesis robustness tests found two error types escaping the command line; the first Clang sanitizer build found a test that only recognised GCC’s sanitizer macros. None of them was visible to the unit tests, the kit or parity, which is the case for running them in CI rather than once.
Instrumentation needs the runtime’s help. TSan with OpenMP is noise without Archer, and with Archer it needs
ignore_noninstrumented_modules; a check that is noisy by construction gets turned off, so the setup was verified to report a planted race and to stay quiet on correct code before it went into a workflow.A version promise lives in many places. The re-grouping touched some forty files: tables generated from
planned.tomlupdated themselves, every hand-written “(0.3)” did not. Planned versions in docstrings and messages are best named by milestone or left out.Parallel milestones need disjoint files. M6a and M6b both wrote ADR 0030, both append to the approvals log, the CHANGELOG and the README; keeping edits additive and leaving the other milestone’s topics alone (the plugin wording,
pyproject.toml) keeps the merge small.A kit tests only the batches its generator can make. The conformance kit was green on
dynamic_bfsand on the tutorial’smy_bfswhile both gave wrong levels for an edge inserted and deleted in one batch, because the generator never made such a batch and no preset applied the insertions first; a documented name (“the effective changes”) hid that the delta lists requested operations. The review added the batches and the preset; a new batch-semantics switch needs a preset of its own in the kit.A check of a tutorial must follow its prose. Copying the finished reference files proved that the reference works, not that the steps do; the scaffold check now makes the edits where the prose puts them.
The local toolchain differs from the hosted one. The conda Clang needs
--gcc-install-dir,--sysroot=/and an rpath to use the system libstdc++ and its own libomp (a wrapper script in the scratch directory); the hosted jobs install Ubuntu’sclang-18,libomp-18-devandlibclang-rt-18-devand need none of it.
State of 0.2.0 from M6b’s side#
Ready, with these steps before the release:
Merge
m6b-hardeningandm6a-cuda-wheelsintomain(the orchestrator). The ADR numbers no longer collide (M6a: 0030-0032; M6b: 0033, 0034). Expected conflicts, each to be resolved by keeping both sides:GOVERNANCE.md: both add approvals-log rows after the 2026-10-02 rows, and in “Open decisions” M6b removed the ADR 0029 row (accepted) where M6a added the plugin-licence row after it: keep M6a’s new row, drop the ADR 0029 row.docs/adr/README.md: M6b changed the ADR 0029 row and added 0033 and 0034 where M6a added 0030-0032: keep the 0029 row as accepted, then 0030-0034 in order.docs/developer/release.md: M6b changed the role-table row “Zenodo, conda-forge …” (no Read the Docs) next to M6a’s changed approval row, and added a paragraph under “Checklist”: keep both changes.CHANGELOG.md([Unreleased]) andREADME.md: both added entries and status texts in different places; keep both.python/CMakeLists.txtandpython/dyng/__init__.py: the two tutorial modules join M6a’s plugin module list.After the merge, the texts that still promise the CUDA plugins in “0.1.x” should say 0.2.0:
python/dyng/cli/_common.py(the--backendhelp, M6a’s file;docs/api/cli.mdis fixed here) and whatever M6a’s rewrite ofdocs/getting_started/install.md,docs/getting_started/index.md,docs/api/python/index.md,python/dyng/config.py,python/dyng/resources.pyanddocs/developer/wheels.mdleaves behind (git grep -n '0\.1\.x').
The first hosted runs of
sanitizers.yml,fuzz.yml,property.ymland thedeployjob ofdocs.yml. Pages is enabled (2026-10-06), so the first push tomainafter the merge deploys the site; then set the repository website (repository_settings.mdstep 13, item 4).Decide whether the three sanitizer checks become required (
repository_settings.mdstep 9).The release checklist (
docs/developer/release.md): step 3 now names the sanitizer presets andci/sanitizers.sh; the mutation record of M6b isparity/results/M6b-mutation.json.
Nothing in M6b changes a gated code path of sssp, cycle_count or mosp (the executors and the kit’s undirected models are used by the tutorials and the tests; the reader fixes only bound reservations), so the 0.2 performance records of M7 stand.
Re-estimate#
M6b took one working day for its three steps (the commits are of 2026-10-06), against the
plan’s 4-7 focused days for the whole M6 (M6a in parallel). The longest parts were the fresh-clone verifications, the 27-minute mutation run and the
15-minute fuzz runs, all behind the shared lock with other agents’ builds. docs/developer/plan.md
section 4 carries the new release rows: 0.2.0 (M7 done, M6b done, M6a in parallel) about 1-2 weeks
of calendar time from 2026-10-06, dominated by the merges, the first hosted runs and the release
steps; 0.3.0 (M8, M9) 1-1.5 focused weeks; 0.4.0 (M10, M11) 1.5-2.5 focused weeks.
Open items for the lead maintainer and the orchestrator#
ADR 0029 is done: accepted in the review with the author’s decision of 2026-10-06 (
6fab3d9).Hosted runs not yet seen (never pushed):
sanitizers.yml(in particular the Archer path of Ubuntu’slibomp5-18, found by the workflow under/usr/lib/llvm-18, and themmap_rnd_bitsstep),fuzz.yml,property.ymland the Pages deployment. Each passed actionlint, zizmor and the meta check, and its script ran locally.pyproject.toml(M6a’s): addDocumentation = "https://dyng-dev.github.io/dyng/"under[project.urls]; optionally excludecpp/fuzz/from the sdist (about 20 KB).The ADR number collision is resolved (M6b’s ADRs are 0033 and 0034); the other merge items are in “State of 0.2.0” item 1.
Remaining “0.1.x” mentions of the CUDA plugins: see “State of 0.2.0” item 1 (the CLI help and the pages M6a rewrites);
docs/api/cli.mdis fixed here.Tutorial limitations (ADR 0033):
dynamic_bfscopies all levels once per update to computestats.affected(O(n)) and has no performance gate.