ADR 0047: The hypergraph API review and freeze (0.3)#
Status: Accepted under delegation (2026-10-14; GOVERNANCE.md, “Delegation of technical ADRs”) for the review, its additions and the freeze mechanism. It carries out PLAN 11.3’s “the hypergraph headers frozen” (the M8 row) at the end of M8b, by the method of ADR 0023 and the checklist
docs/developer/api_review_checklist.md. It changes no rule the author approved: the stability policy of PLAN 5.9, the gates and their protocol, the parity rules and the names PLAN fixes (hypergraph,hypergraph_storage::escher,line_graph_policy::sorted_merge,id_reuse,capacity_policy::escher_gpu()/mosp_escher(),hyper_apply_summary) are untouched. Pending the author: that the frozen hypergraph headers are stable from 0.3.0 (and.hgand.dgbstable,.dgbat format version 1,.hgunversioned: ADR 0045) is the plan’s default and awaits the author’s confirmation at the 0.3.0 release; O16 (theid_reusedefaulterase_first) is the author’s decision of 2026-10-07, which is also the plan’s default.Date: 2026-10-14
Deciders: the AI assistant, on the author’s behalf (M8b, step python-freeze-records)
Builds on: ADR 0023 (the method, the snapshot), ADR 0035 (the freeze enforced against the base branch), ADRs 0041-0046 (the container, the line graph, the update path, the scaffold, the files and generators, the Python layer).
Context#
M8b built the hypergraph container on the ESCHER store and froze nothing until its last step:
its headers were tracked in the API baseline from their first commit (ADR 0041). PLAN 11.3
freezes them at M8b, so that M9 (triad_count, 0.3) and M11 (hyper_sssp and the slack_csr
storage, 0.4) build on a fixed container API. Before the freeze the API is reviewed as the 0.1
and mosp APIs were (ADRs 0023, 0035), and its fitness for the two later milestones is proven by
code that compiles against it.
Reviewed: every public entity of cpp/include/dyng/hypergraph/ (hypergraph_properties.hpp,
hyperedge_list.hpp, hypergraph_view.hpp, hyperedge_batch.hpp, hyper_apply_summary.hpp,
hypergraph.hpp with its detail contract: participant_of<hypergraph<V, W>>,
hyper_update_participant, run_update, is_hypergraph_v), the tracked hypergraph parts of
io/hypergraph_io.hpp, the .dgb parts of io/batch_io.hpp, generators/hypergraphs.hpp, the
ESCHER part of generators/legacy.hpp, testing/cbst_host_model.hpp and
testing/line_graph_oracle.hpp, the include list of dyng/dyng.hpp, and the Python layer of ADR
0046 (dyng.Hypergraph and its companions, dyng.io, dyng.generators, dyng.testing).
Findings and what was done#
# |
Area |
Finding |
Resolution |
|---|---|---|---|
1 |
Naming |
The names follow ADR 0004 (clang-tidy’s naming rules pass): |
none needed |
2 |
The verb vocabulary |
|
Accepted (ADR 0023 finding 2) |
3 |
Ids and sentinels (PLAN 4.4.2) |
Ids are 0-based everywhere, -1 means none, hyperedge ids and every offset are |
Reworded (“its members plus one slot, rounded up to a multiple of this”): documentation only |
4 |
Printable enumerations |
|
none needed |
5 |
Planned features |
|
none needed |
6 |
The four-op batch |
|
Added |
7 |
Exception specifications |
|
Declared |
8 |
Doxygen |
Doxygen with |
|
9 |
Exception guarantees |
|
none needed |
10 |
Host-compilability |
Every hypergraph header compiles alone without CUDA, CUB, Thrust or libcu++ (the self-containment targets, ADR 0023 finding 9). |
none needed |
11 |
Ownership and ABI |
|
Accepted for 0.x; the order is frozen as it is (positional initialization of |
12 |
Views and versions |
Every view carries the |
Documented in the header (step 2); accepted |
13 |
|
Can go down after a surplus rebuild (ADR 0041, deviation 10). |
Documented in the header; accepted |
14 |
M9’s diagnostic (ADR 0035 finding 16) |
A graph result passed to |
Not changed: |
15 |
|
Throws |
Accepted for the testing tier (tracked): documented in its header; the Python binding raises |
16 |
io, generators, testing |
|
Accepted; these headers stay tracked (PLAN 5.9: io, generators and testing are frozen with the CLI and the Python layer as a whole, not with the container). |
17 |
Python |
ADR 0046: the C++ names and defaults (tested equal to the native defaults), enumerations as strings, |
none needed |
Fitness for M9 and M11 (compile-only skeletons)#
cpp/tests/api/fitness/ (the OBJECT library dyng_api_fitness, compiled in every preset, never
run) holds what the next milestones need, written against the frozen headers and the internal
access M9 and M11 are allowed (cpp/src, internal-stable):
M9,
m9_triad_count_fitness.cpp: a triad participant ofdyng::update()throughupdate_traitsandparticipant_of<hypergraph<V, W>>that counts (-) inbefore_apply()on the oldh2h()/h2v()rows (0-based,int64offsets, no sentinels, in the hypergraph’s memory space) with the frontier = the live deleted ids of Step 0 (use_normalized), reads the internal row views of ESCHER-GPU’s layout (hypergraph_access::views(), afterrequire_line_graph()), and counts (+) inafter_apply()withinserted_idson the host and in device memory (hyper_applied::inserted_ids_device), the deleted member sets and the touched sets; pairs are taken fromout[r]on (each triad by its smallest member); the replay of a.dgbfile by key (expected_idsagainstinserted_ids) and by content (the deleted member sets resolved againsth2v()), on sequential, openmp and cuda resources.M11,
m11_hyper_sssp_fitness.cpp: the four-op batch (delete_hyperedge,insert_hyperedge,delete_incidence,insert_incidence, and the view’s arrays);id_reuse::insert_firstwithcapacity_policy::mosp_escher(extra);(int32, int32)with a weight of 2^28;line_graph_view::weights(node weights) andoverlaps; theslack_csrenumerators and aslack_csr_storeimplementing every member ofdetail::hyper::incidence_store; the incidence-delta hook (hypergraph_access::apply(..., true),hyper_applied:: incidence_delta).
Decision#
The hypergraph headers are frozen as reviewed, with the additions of findings 6 and 7: every declaration of
dyng/hypergraph/*.hpp(six sections of the baseline, thedetailcontract ofhypergraph.hppincluded).ci/api_snapshot.pylistsdyng/hypergraph/*with the frozen core;io/hypergraph_io.hpp,io/batch_io.hpp,generators/hypergraphs.hpp,generators/legacy.hppandtesting/*stay tracked. From the freeze a change of a frozen hypergraph declaration failsci/docs.sh(against the baseline) andci/api_check.sh(against the base branch) unless the pull request has theapi-changelabel and a CHANGELOG entry. The Python names of ADR 0046 are checked bygriffein the same script.Stability. By the plan’s default the frozen headers are stable from 0.3.0 (PLAN 11.3, 5.9), with
.dgbat format version 1 and.hgwithout a version field (amended, ADR 0045 section 1); this awaits the author’s confirmation at the 0.3.0 release (GOVERNANCE.md, open decisions). Until 0.3.0 is released a change is still possible as an api-change with a CHANGELOG entry.O16. The
id_reusedefault stayserase_first: the author’s decision of 2026-10-07 (GOVERNANCE.md), which is the plan’s default (PLAN 12.2). Changing it before 0.3.0 would be an api-change (a changed default of a frozen declaration; the baseline lineid_reuse reuse = id_reuse::erase_firstandci/tests/test_api_snapshot.pypin it).Both API checks fail on a deliberate hypergraph change, shown on 2026-10-14 in a throwaway clone of this branch at
753449e(the freeze commit) and reverted (the logs are in$DYNG_SCRATCH/runs/m8b-step5/demo-*.log):C++: the default of
hypergraph::from_hyperedges(..., const hypergraph_properties& props = {})removed:ci/docs.sh --doxygen-onlyexits 1 withapi-snapshot: the public C++ API differs from the baseline (above: - baseline, + headers). - [[nodiscard]] static hypergraph from_hyperedges(const resources &res, hyperedge_list_view< vertex_t, weight_t > hyperedges, const hypergraph_properties &props={}) + [[nodiscard]] static hypergraph from_hyperedges(const resources &res, hyperedge_list_view< vertex_t, weight_t > hyperedges, const hypergraph_properties &props)and, with the baseline regenerated (
ci/docs.sh --update-api, as a pull request would),ci/api_check.sh 753449eexits 1:breaking change of the frozen API: [dyng/hypergraph/hypergraph.hpp] - [[nodiscard]] static hypergraph from_hyperedges(...).Python:
dyng.Hypergraph.from_lists(..., id_reuse=...)renamedreuse=: the sameci/api_check.sh 753449eexits 1 withHypergraph.from_lists(id_reuse): Parameter was removed.Reverted,
ci/api_check.sh 753449epasses (“no breaking change” for both halves) andci/docs.sh --doxygen-onlyreports1095 declarations match public_api.txt.ci/tests/test_api_snapshot.py::test_the_hypergraph_headers_are_frozen_and_a_change_is_breakingpins the frozen labels and a breaking change of theid_reusedefault.
Consequences#
M9 and M11 build on a fixed container API; their headers (
triad_count.hpp,hyper_sssp.hpp) are frozen by their own milestones (their manifests’ maturity). Additions to the hypergraph headers (an appended field, a new accessor) are reported by the API check, not failed; a removal or change needs the label.The pull request of M8b needs the
api-changelabel (the hypergraph headers change from tracked to frozen and gain four accessors); its CHANGELOG entry says so.
Amendments of the M8b review (2026-10-14)#
Independent reviewers checked the frozen headers’ semantics against the code before the pull request; each amendment below keeps the decisions above and corrects what the freeze records.
Finding 3,
capacity_policy::row_alignment. The reworded comment (“its members plus one slot, rounded up to a multiple of this”,ceil((n + 1) / a) * a) contradicted the allocator (aligned(n, a) + 1, ADR 0041 rule 2:ceil(n / a) * a + 1; 5 slots, not 8, for n = a = 4), and the Python docstring still said “terminator”. Both now say: a surplus row inserted by a batch reserves its members rounded up to a multiple of this, plus one slot; the concepts page says that its worked example is the construction layout, whichrow_alignmentdoes not set.Finding 12,
line_graph_view::weights. The finding said the host memory ofweightswas documented in the header, but onlyhypergraph::h2h()andweights()said so; the frozen struct’s own comment said “the arrays live in the container’s memory space”. The struct and the field now say thatweightsis host memory on every backend (the decision stands; M11’s device weights forhyper_ssspare a separate device copy, ADR 0041 section 3).Finding 5,
skipped_ops. The field was documented asignored_deletions + dropped_members, which left no room for the incidence-edit skips that M11 must count (B’sskippedOpsparity, check C9), so “M11 adds behaviour, not declarations” did not hold. The field is now documented as every skipped operation (in 0.3 that sum), and ADR 0041 section 8 records the 0.4 rule: incidence edits that B skips are skipped and counted, an out-of-range vertex in an incidence edit included.Decision 1, the
detailcontract. The decision freezes thedetailcontract ofhypergraph.hpp(participant_of,hyper_update_participant,run_update,is_hypergraph_v), butci/api_snapshot.pyread onlyparticipant_of/update_traits/stats_ofand the*_supported_vtraits fromdetail, and Doxygen excludesdyng::detail: a changed virtual signature ofhyper_update_participantpassed both checks. The snapshot now also lists theis_hypergraphtrait,is_hypergraph_v, the aliases and virtual members ofhyper_update_participantand the declaration ofrun_update()(fivedetail:lines in the frozenhypergraph.hppsection;ci/tests/test_api_snapshot.pychecks the extraction). The graph’supdate_participant(update.hpp) stays out of the listing: it is internal-stable (ADR 0023), andupdate.hppis unchanged by M8b. The types the interface names (hyper_staged_batch,hyper_applied) are internal-stable records, not part of the freeze. The deliberate change was shown again (2026-10-14, this branch, reverted; logs in$DYNG_SCRATCH/runs/m8b-review/demo-detail-*.log):after_apply()without itssummaryparameter failsci/docs.sh --doxygen-only(the listing differs from the baseline), and with the baseline regeneratedci/api_check.shagainst the commit that added the lines reports a breaking change of[dyng/hypergraph/hypergraph.hpp].