ADR 0043: The hypergraph’s update path and the conformance kit’s container axis#
Status: Accepted under delegation (2026-10-14; GOVERNANCE.md, “Delegation of technical ADRs”). It carries out M8b step 3 (PLAN 4.5.1, 4.5.5 I1 and I9, 8.2, 8.4; the M8b acceptance criteria 10 and 11; ADR 0023 amendment A1) and changes no rule the author approved: the frozen
update.hppand the graph headers stay byte-identical to main, the parity rules, the gates and their protocol are untouched, and the graph suites of the kit keep their behaviour and test counts.Date: 2026-10-14
Deciders: the AI assistant, on the author’s behalf (M8b, step update-kit-scaffold)
Context#
ADR 0023 A1 made dyng::update() dispatch through the class template
detail::participant_of<container_t>, so that the hypergraph could join without editing the
frozen update.hpp. ADR 0041 left the applied record as container state “until dyng::update
(step 4) moves it into its transaction” (its deviation 12) and gave hypergraph::apply() one
body from validation to commit. The framework’s adapter (problem_participant), its
expect_current_result / stamp_result and the conformance kit (C0-C12, ADR 0022) were written
for graph<V,E,W>: the kit’s case names read edge_type, its registration needs an
edge_batch_view and an apply_summary, C0 asserts container_kind::graph, its presets, model,
eight batch mixes and C7/C9/C11 are graph-specific. M9’s triad_count needs all of it on the
hypergraph: a problem that subtracts on G_t through the deleted ids and adds on G_{t+1} through the
inserted ids, run by dyng::update(), checked by the kit.
Decision#
1. The hypergraph transaction (cpp/src/hypergraph/transaction.hpp)#
detail::hyper_transaction<V,W> is the container’s internal transaction of PLAN 4.5.1:
Phase |
What it does |
Guarantee |
|---|---|---|
begin (constructor) |
placement and poisoning checks; incidence edits refused (0.4); the batch staged to the host once under the copy policy; Step 0 once: inserted rows sorted and deduplicated (dropped members counted), weights checked, deletions checked against G_t (dead, out-of-range, negative and repeated ids skipped and counted) into |
strong: nothing changes |
before() |
an |
– |
commit() |
the store phase in the order of |
basic: a store error poisons the hypergraph |
after(), applied() |
a |
– |
hypergraph::apply() is a transaction without participants (its stages are unchanged:
hypergraph.apply around begin and commit). before() after the commit throws
internal_error; the old_view itself checks its version in Debug builds (invariant I1), so a
problem that keeps G_t past the commit fails there (tests
HypergraphUpdate.AnOldViewUsedAfterTheCommitThrowsInDebugBuilds and
...AProblemThatKeepsItsOldViewPastTheCommitFailsInDebugBuilds). The transaction copies the
resources handle (shared, cheap): a caller’s temporary must not dangle.
The applied record stays container state (ADR 0041 deviation 12 is resolved this way, not by
moving it): applied() returns the record of the commit it made, which is the hypergraph’s last
record. Moving it into the transaction would take it from hypergraph_access::applied(), which
the parity tools and M11’s incidence-delta hook read after a plain apply(). It carries the
inserted ids on the host and (CUDA) the device, the deleted ids with their member sets, the
touched hyperedges and vertices, the skipped counts and the incidence-delta hook (unchanged from
ADR 0041 section 10).
2. dyng::update() over hypergraphs#
<dyng/hypergraph/hypergraph.hpp>includes<dyng/update.hpp>and specializesdetail::participant_of<hypergraph<V,W>>with the participant interfacedetail::hyper_update_participant<V,W>(target,use_normalized(const hyper_staged_batch<W>*),before_apply(res, h, batch),after_apply(res, h, hyper_apply_summary, hyper_applied),poison,reads_prepared_graph), its batch viewhyperedge_batch_view<V,W>andrun().update.hppandcpp/include/dyng/graph/*are unchanged.detail::run_update(res, h, batch, participants, n, commit_stage)for hypergraphs (cpp/src/hypergraph/hyper_update.cpp): duplicate targets rejected; the transaction’s begin under the stage<algo>.normalize(update.normalize); the staged batch handed to every participant; the line graph made sure of when a participantreads_prepared_graph()(built on G_t underline_graph_policy::automatic, aninvalid_argument_errorundernone, both before anything changes); everybefore_applyon G_t; one commit insidebudget_scope(<commit_stage>aroundhypergraph.apply; container growth is reported throughlast_commit_counts(), never failed); everyafter_applyon G_{t+1}, a failing participant poisoned while the others still run, the first exception rethrown.Preparing the views. The internal row views (h2v and, when maintained, h2h) are rebuilt by every commit; the public views stay lazy (never built on the apply path, ADR 0042). So “prepare” for hypergraphs means the line graph only, and
reads_prepared_graph()means “reads the line graph” (default false for hypergraph problems, true for graph problems as before).detail::is_hypergraph_v<T>(public, inhypergraph.hpp) lets a hypergraph algorithm’supdate_traitssay in plain English that it takes a hypergraph (the scaffold’s template does).
3. The framework#
framework/composition.hpp serves both containers: composition_detail::graph_types<container>
names, per container, the participant interface, the batch view, what the hooks see before
(hyper_requested_batch {hyperedges, normalized}) and after (hyper_applied_batch {summary, record, normalized}) the commit, Step 0’s normalized batch, the summary type of stats::batch
(has_batch_summary takes it) and the record after_apply receives. problem_participant,
make_participant and update_one are unchanged in shape. expect_current_result and
stamp_result read the state identity through state_identity() overloads, and the messages
name the container (“hypergraph version 3”); the graph’s messages are byte-identical.
4. The kit’s container axis#
container_traits<container_t> (cpp/tests/conformance/containers.hpp; the hypergraph’s in
hypergraph_generators.hpp) gives the checks the container’s properties and presets, its host
model and batch mixes, the batch and summary types, the summary’s counters, what an empty batch
must leave at zero, the model check, the inverse of a batch (C5), the composition presets and
mixes (C10), and the mixes of C11 and C12. What is too different to be data, C7’s invalid batches
and skip rules and C9’s summary arithmetic, is container_checks<case_t, container_t>; the
graph’s bodies moved there verbatim. A test_traits may name container_types (else
graph_types, the name of 0.1-0.2), registry_entry() (a test-only algorithm the library does
not register) and test_partners (C10’s test-only partners).
Graph suites unchanged. Same mixes, seeds, presets, checks and order of steps; the test lists of every graph conformance executable (host and CUDA) are identical to the branch base’s, and they pass.
The hypergraph presets: {erase_first, insert_first} x {escher_gpu(), mosp_escher(), the default}, each with the traits’
require(), plus the traits’ extras.The hypergraph model keeps the live hyperedges by id and the id bound. A batch’s deletions name live ids (or, in the skip-rule mixes, dead, out-of-range, negative and repeated ones); the inserted ids are the container’s, recorded from
hyper_apply_summary::inserted_idsbyobserve()after the step, never predicted. The inverse of a batch (C5) is built from that summary and the deleted member sets: delete the inserted ids, insert the deleted hyperedges again.The mixes (criterion 11): insertions only, deletions only, mixed, delete-then-reuse within one batch, duplicate members (and copies of live hyperedges), dead-id deletions, empty, delete-all, re-insert, a batch larger than the hypergraph, ids at the index limits (members 0 and n - 1, the largest live id, INT64_MIN / INT64_MAX deletions), skewed cardinality (>= 1,024 on the medium shapes).
HypergraphKitGenerators.EveryMixMakesWhatItsNamePromiseschecks that each mix makes what its name says (M6b: a kit tests only what its generator makes).C7 (hypergraph): a member below 0 or at n, an empty hyperedge, malformed offsets, a bad weight (weights on an unweighted batch) are rejected with
invalid_argument_errorand an incidence edit withnot_supported_error, each before anything changes (version, live count, bound, result); dead, out-of-range, negative and repeated deletions and repeated members are counted.C9 (hypergraph): applied + skipped == requested;
ignored_deletionsanddropped_membersequal what the generator put in;skipped_opstheir sum; the inserted ids distinct, below the bound, each holding the inserted hyperedge (read back throughh2v()).
5. The test-only fakes and the planted bug#
cpp/tests/framework/fake_hyper_problems.hpp: hyper_pairs_problem (the overlapping pairs of
live hyperedges, the edges of h2h) and hyper_squares_problem (the sum of |e|^2), both
aggregate delta with ownership::min_member: count(-) on the OLD row views with frontier = the
deleted ids (Step 0’s, in before_apply), count(+) on the NEW row views with frontier =
inserted_ids (on CUDA the applied record’s device copy, read by a kernel). Their kit
(dyng_hypergraph_kit_tests, ..._cuda_tests) runs every applicable check on sequential, openmp
and cuda: C5 with the inverse from the summary, C10 with the two fakes on one container, C11, C12
on two non-blocking streams, C8 with 0 synchronizations on the host and 2 on CUDA. C4 is skipped
(one engine). The planted bug DYNG_MUTATION_KIT_FRONTIER_REQUESTED_IDS (frontier =
id_bound(G_t) + i, the ids the request would get without reuse) fails C2, C5 and C7 (and C8 in
budgets builds) on every backend; it is the CTest conformance.mutation.frontier_requested_ids
and the suite kit of parity/mutate.py.
6. Gaps the kit exposed, fixed or recorded before the freeze#
# |
Finding |
Resolution |
|---|---|---|
1 |
The host stores built the lazy public views (h2v, v2h, h2h) and the |
Fixed: the host store counts them as container work too (as ADR 0022’s container work of graphs). |
2 |
A hypergraph algorithm’s result passed with a graph had no plain-English error. |
|
3 |
The frozen |
Recorded, not changed: |
4 |
A graph algorithm’s result passed with a hypergraph stops inside the graph algorithm’s frozen |
Recorded (sssp.hpp is frozen); a compile-fail test pins that it does not compile. |
5 |
|
The transaction (section 1). |
6 |
The kit could not run an algorithm the library does not register. |
|
Consequences#
dyng::update(res, h, batch, r1, r2)andupdate_eachwork for hypergraph algorithms with one Step 0 and one commit; M9’striad_countplugs in throughmake_participantlike a graph algorithm, and its traits through the kit’s hypergraph axis with no kit change (ci/scaffold_check.shproves it with two scaffolded hypergraph algorithms).<dyng/hypergraph/hypergraph.hpp>gainsdetail::hyper_update_participant,run_update,participant_of<hypergraph>andis_hypergraph; the API baseline records them (tracked until the freeze of step 5).The stage names of a hypergraph update:
update.normalize(or<algo>.normalize),update.commit(or<algo>.commit) containinghypergraph.applyand its sub-stages.