ADR 0023: The 0.1 API review and freeze#
Status: Accepted under delegation (2026-09-29; GOVERNANCE.md, “Delegation of technical ADRs”). It records the review of the 0.1 headers that PLAN Section 11.2 (M3) asks for, the fixes it led to, and the mechanism that freezes the result. It does not change a rule the author approved: the stability policy of PLAN Section 5.9, the performance gates, the measurement protocol (ADRs 0018, 0021) and the parity rules are untouched. With this ADR the algorithm contract of ADR 0006 is accepted as the frozen 0.1 contract.
Date: 2026-09-29
Deciders: the AI assistant, on the author’s behalf (M3, step api-freeze)
Context#
PLAN Section 5 freezes the 0.1 headers in M3, after sssp and cycle_count run through the
framework on all three backends: core/*, graph/*, update.hpp, sssp.hpp and
cycle_count.hpp. Until then they evolved with the two ports. The freeze is preceded by a review
against docs/developer/api_review_checklist.md: naming (PLAN 4.4, ADR 0004), the uniformity of
the algorithm contract (PLAN 5.1, ADR 0006), Doxygen completeness (PLAN 9.1), host-compilability
(PLAN 4.1), ownership and ABI (pimpl), exception guarantees (PLAN 4.7.3), and anything that would
hurt the Python bindings of M5 (PLAN 5.4). After the freeze a change of these headers is an API
change under PLAN 5.9, so the freeze needs a check that notices one.
The review covered every header under cpp/include/dyng (the top-level dyng.hpp,
citation.hpp, version.hpp, config.hpp with the core ones). io/*, generators/* and
testing/* were read but are not frozen in 0.1: their shape is decided together with the CLI and
the Python layer in M5 (PLAN 5.6-5.8).
Findings and what was done#
# |
Area |
Finding |
Resolution |
|---|---|---|---|
1 |
Naming |
clang-tidy’s naming rules (ADR 0004) pass on every header; no |
none needed |
2 |
Naming: the verb vocabulary |
Verbs outside the fixed list: |
Accepted: each is a container, builder or recording operation the vocabulary does not cover, or is named by the plan. |
3 |
Naming: |
|
Accepted: each would clash with a type of the same name (the rule’s exception). |
4 |
Contract uniformity |
Both algorithms have |
Accepted as the rule: a result is a template only when its arrays’ types depend on the graph’s; importing a result is per algorithm. |
5 |
Printable enumerations |
|
Added |
6 |
Doxygen |
|
The checker reads the algorithm namespaces from the manifests; |
7 |
Exception guarantees |
PLAN 4.7.3 asks each function to document strong or basic; the headers said “nothing is changed” on some |
A |
8 |
Exceptions leaving the library |
|
Both translate |
9 |
Host-compilability |
The self-containment targets compiled each header alone, but a CUDA build has the CUDA include directories on their command line, so a stray |
The generated units fail with |
10 |
Ownership and ABI |
|
Accepted for 0.x. Before 1.0 (when |
11 |
Macros in public headers |
|
Accepted; all |
12 |
The registry header (ADR 0022) |
|
Accepted as frozen. |
Notes for the Python bindings (M5)#
Nothing in the 0.1 headers blocks nanobind, but these need a deliberate binding, recorded here so M5 does not rediscover them:
dyng::update(res, g, batch, results...)is variadic. Python’sdyng.update(g, b, r1, r2)takes a run-time list of results of mixed types; the binding builds the type-erased participants itself and callsdetail::run_update()(the same pathupdate()takes).The views returned by
result::distances(),parents(),counts()andgraph::view()are valid until the next update or apply.dyng.Arraymust keep its owner alive and either check the owner’s version on access or copy (PLAN 5.4 rule 4 andcopy=False).std::initializer_listoverloads (edge_batch::insert_edge,edge_list::add_edge) are not bound; thearray_viewoverloads are.resourcescopies share one handle, and a default CUDA stream is per thread: two Python threads using oneResourcesrun on two streams (the class documentation says so).profilerrecording is thread-safe (amended in the M3 review: it was not, although copies of aresourceshandle, which share its profiler, may be used concurrently). Holding the GIL while reading the records is not enough, because the binding releases the GIL around every native call (PLAN 5.4 rule 3) and the records are written during those calls: the binding readsp.stagesetc. only when no call through a handle with that profiler is running, or copies the records under its own lock after each call.The enumerations accept their
to_stringnames (finding 5).
Decision#
The 0.1 API is frozen as reviewed above: every declaration of
core/*,graph/*,update.hpp,sssp.hpp,cycle_count.hppand the top-leveldyng.hpp,citation.hpp,version.hpp,config.hpp.io/*,generators/*andtesting/*are tracked (checked the same way) and are reviewed and frozen with the CLI and the Python layer in M5. The algorithm contract of ADR 0006 is accepted as part of the freeze.The freeze is a committed listing, not an ABI dump.
ci/api_snapshot.pygenerates a listing of every public declaration from the Doxygen XML thatci/docs.shalready produces (signatures with default arguments, template parameters, bases, public members with their initializers, enumerators, type aliases, macro names; no comments, line numbers or bodies) and compares it withcpp/tests/api/api_snapshot/public_api.txt.ci/docs.shruns the check after Doxygen, so thedocsworkflow and the local gate fail on any change the baseline does not contain; a changed default value fails too (a breaking change under PLAN 5.9). This is the “C++ public-header snapshot (from Doxygen XML)” of PLAN 5.9.abi-dumper/ libabigail are not used before 1.0: most of the API is templates instantiated in the user’s code, which an ABI dump oflibdyng.sodoes not see, and PLAN 5.9 addsabidiffat 1.0.Changing the API follows “Updating the API baseline” in
docs/developer/api_review_checklist.md: the checklist, a CHANGELOG entry and theapi-changelabel, thenci/docs.sh --update-apiand the new baseline in the same pull request. (Amended in the M3 review: the first text,ci/docs.sh --doxygen-only && ci/api_snapshot.py --update, could never update the baseline, becauseci/docs.shruns the check itself and fails while the API differs;--update-apiruns Doxygen and writes the baseline.) Theapi-check.ymlworkflow of M5 adds the label check andgriffe.Every function that changes a container or a result states its exception guarantee with
@guarantee, checked byci/doxygen_coverage.py; host allocation failures leave the library asout_of_memory_error, also from header-inline code.Public headers include no CUDA header, checked by the self-containment targets in every build.
Reviewed sketches of the later algorithms’ APIs (
mosp, the hypergraph withhyperedge_batch,triad_count,label_propagation,hyper_sssp) are indocs/design/sketches/, written against the frozen contract. They are not contracts; each is frozen by the milestone that implements it.
Amendments after the M3 review (2026-09-29, accepted under delegation)#
Independent reviewers of the M3 branch found gaps in this review. The fixes below change frozen headers before 0.1 is tagged (nothing was released), each with a baseline diff and a CHANGELOG entry (“Fixed (M3 review)”). None changes a rule the author approved.
# |
Finding |
Resolution |
|---|---|---|
A1 |
|
The dispatch is the class template |
A2 |
Finding 4 called the contract uniform, but an unsupported graph type failed to link for sssp and stopped at a |
|
A3 |
Finding 5 left ten frozen enumerations without |
Added, with tests. |
A4 |
Finding 7 required |
The builders are strong (they roll back); every mutating member of the container, batch and result classes states its guarantee, and |
A5 |
Finding 8 missed |
Translated to |
A6 |
The API snapshot did not see the |
|
A7 |
The profiler was unsynchronized although copies of a handle, which share it, may be used concurrently (and note 5 relied on the GIL, which native calls release). |
Recording is serialized, with a per-thread stage stack; the rule for reading the records is documented, note 5 corrected. |
A8 |
The threading rules of PLAN 4.7.4 for graphs and results were not documented in the frozen headers. |
“Thread safety” paragraphs on |
A9 |
|
|
A10 |
Documentation: 23 references to the unpublished plan in the public headers; |
Replaced by the rule itself or a published page; “deterministic per backend” defined and used; texts corrected. |
A11 |
The documented baseline update ( |
|
Consequences#
An accidental change of a public signature, default, field or enumerator now fails CI with a diff instead of reaching a release; a deliberate one carries its own baseline diff.
The listing depends on the Doxygen version (pinned in
environment.yml, 1.18.0). A Doxygen upgrade that changes how signatures are printed needs one--updatein its own commit.The frozen headers can still gain declarations (PLAN 5.9’s additive rules); each addition updates the baseline, so the reviewers see it.
The
profilerpimpl, the Python items above and the review ofio/*,generators/*andtesting/*are open work for M5 and before 1.0 (the retrospective lists them).