Skip to content

Add Edmonds-Blossom, weighted matching, Hungarian algorithm, LCA, and tree decomposition implementations#36

Merged
lrleon merged 28 commits intomasterfrom
graph-round-3
Feb 23, 2026
Merged

Add Edmonds-Blossom, weighted matching, Hungarian algorithm, LCA, and tree decomposition implementations#36
lrleon merged 28 commits intomasterfrom
graph-round-3

Conversation

@lrleon
Copy link
Copy Markdown
Owner

@lrleon lrleon commented Feb 21, 2026

Summary by CodeRabbit

  • New Features

    • Added general matching (unweighted & weighted), min‑cost & perfect matching, Hungarian assignment, k‑shortest paths (Yen/Eppstein), Karp min‑mean cycle, dominators/post‑dominators, planarity testing & certificates, LCA engines, Heavy‑Light & centroid decompositions and tree‑decomposition query APIs.
  • Examples

    • Many new demos: matching (weighted/unweighted), min‑cost matching, HLD/centroid, LCA, k‑shortest, planarity (TikZ/exports), dominators, Hungarian, and more.
  • Tests

    • Extensive cross‑backend test suites validating all added features.
  • Documentation

    • README (EN/ES) substantially expanded to cover new APIs, examples, and usage.

Copilot AI review requested due to automatic review settings February 21, 2026 16:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds many new graph algorithms, matchers, decompositions, examples, tests, CI jobs, and container utilities: unweighted/weighted Edmonds blossom, min-cost/perfect matching wrappers, Hungarian assignment, k-shortest paths, Karp min-mean-cycle, LCA engines, HLD/centroid decompositions, Tarjan bridges, Lengauer–Tarjan dominators, plus examples/tests and CMake/CI updates.

Changes

Cohort / File(s) Summary
Blossom (cardinality)
Blossom.H
New Edmonds unweighted blossom matcher: internal matcher, public compute_maximum_cardinality_general_matching, alias, functor; returns matched Arc* list; validates undirected graphs.
Blossom (weighted) — public API
Blossom_Weighted.H
Public weighted-blossom API and result type, compute_maximum_weight_general_matching + alias and functor; weight conversion, deduplication, optional max-cardinality mode, overflow checks.
Blossom (weighted) — core solver
blossom_weighted_mwmatching.H
Core mwmatching implementation: Edge type, Problem_Data, Blossom/NonTrivialBlossom, MatchingContext (dual/primal logic), verifier, maximum_weight_matching and weight-adjust helpers.
Min-cost / Min-cost-perfect
Min_Cost_Matching.H
Public result types and functions for min-cost/general and perfect matching, cost-to-ll checks, negated-cost adapter, functor wrappers, overflow/domain handling.
Hungarian / Assignment
Hungarian.H
Hungarian (Munkres) implementation: Hungarian_Assignment, Hungarian_Result, rectangular padding, free functions hungarian_assignment and hungarian_max_assignment.
K-shortest paths
K_Shortest_Paths.H, Examples/k_shortest_paths_example.cc
Yen and Eppstein-style k-shortest APIs, suffix-index/path machinery, K_Shortest_Path_Item, examples.
Min-mean-cycle (Karp)
Min_Mean_Cycle.H, Examples/min_mean_cycle_example.cc
Karp algorithm: value & witness variants, result types, DP, witness reconstruction, validation and overflow checks, example.
LCA engines
LCA.H, Tests/lca_test.cc, Examples/lca_example.cc
Binary lifting and Euler+RMQ LCA engines, Rooted_Tree_Data, Euler tour, sparse table, full LCA/kth-ancestor/distance APIs and tests/examples.
HLD core & path queries
HLD.H, Tree_Decomposition.H, Gen_HLD, Gen_HLD_Path_Query
Heavy-Light decomposition engine, segment-tree-backed path/subtree queries (sum/max/min), public aliases HLD_Sum/HLD_Max/HLD_Min, HLD path-query examples and tests.
Centroid decomposition
Tree_Decomposition.H, Examples/centroid_decomposition_example.cc
Centroid decomposition implementation with centroid ancestors/distances and dynamic nearest-center examples/tests.
Bridges & cut-nodes
tpl_cut_nodes.H, Examples/cut_nodes_example.C, Tests/cut_nodes_test.cc
Tarjan bridge finder: Compute_Bridges class and find_bridges free functions; example/demo and tests added.
Dominators / Post-dominators
Dominators.H, Examples/dominators_example.cc, Tests/dominators_test.cc
Lengauer–Tarjan dominator and post-dominator classes and free functions, frontier builders, example and tests.
Examples (many new executables)
Examples/*
Numerous new examples: blossom, weighted_blossom, min_cost_matching, k_shortest_paths, planarity_test, HLD/LCA/Hungarian/centroid/min_mean_cycle/dominators, with TikZ/export helpers and CMake registration.
Tests (extensive)
Tests/*
New/extended unit and randomized tests for blossom, weighted blossom, min-cost matching, Hungarian, k-shortest, LCA, HLD, tree decompositions, min-mean-cycle, planarity, dominators, concept tests, and geometric helper inlines; added to Tests/CMakeLists.
Planarity visual CI
.github/workflows/ci.yml, .github/workflows/planarity_gephi_nightly.yml
New planarity-visual-golden CI job and weekly Gephi workflow for visual regression and artifact comparisons.
Build exports / CMake
CMakeLists.txt, Examples/CMakeLists.txt, Tests/CMakeLists.txt
Added many headers to public HLIST and registered examples/tests.
Container concepts & utilities
ah-concepts.H, ahFunctional.H, ah-dry.H, tpl_sort_utils.H
New concepts AlephSequentialContainer / AlephSequentialIterableContainer; fill/iota helpers and all_unique overloads; minor docs/formatting tweaks.
Documentation
README.md, README.es.md, doxygen_groups.H
README updates describing new APIs/examples and added doxygen group entries for Matching and LCA.
Geometric/test helpers & minor test fixes
Tests/geom_algorithms_test_common.h, assorted Tests/Examples tweaks
Inlined geometric helpers, replaced some size()→length() usages in examples/tests, many small test adjustments (initializations, unused param fixes).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BlossomAPI as Blossom API
    participant Matcher as Edmonds_Blossom_Matcher
    participant BlossomOps as Blossom Contraction/Expansion
    participant BFS as Alternating-Path BFS

    Client->>BlossomAPI: call compute_maximum_cardinality_general_matching(graph, out)
    BlossomAPI->>Matcher: init with graph
    Matcher->>Matcher: build index & adjacency
    loop find augmenting paths
        Matcher->>BFS: search augmenting path (BFS)
        BFS-->>Matcher: path / none
        alt path found with blossom
            Matcher->>BlossomOps: contract blossom (LCA & mark)
            BlossomOps-->>Matcher: contracted graph
            Matcher->>Matcher: augment_matching(path)
        else path found without blossom
            Matcher->>Matcher: augment_matching(path)
        end
    end
    Matcher-->>BlossomAPI: return mate vector
    BlossomAPI->>BlossomAPI: map mates to `Arc*` and return cardinality
Loading
sequenceDiagram
    participant Client
    participant HLDInit as HLD Initialization
    participant Topology as Tree Topology Builder
    participant Decompose as Heavy-Light Decompose
    participant Segment as Segment Tree

    Client->>HLDInit: construct HLD with graph, root, value accessor
    HLDInit->>Topology: index nodes, validate tree
    Topology->>Topology: DFS compute parent, depth, subtree sizes
    Topology-->>Decompose: traversal order
    Decompose->>Decompose: compute heavy child, chain heads, positions
    Decompose->>Segment: build segment tree over HLD order
    Segment-->>HLDInit: ready
    Client->>HLDInit: path_query(u,v)
    HLDInit->>Segment: query segments along path (O(log^2 n))
    Segment-->>Client: aggregated result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Header Docstring Coverage ❓ Inconclusive Automated analysis of header documentation coverage could not be reliably performed due to C++ template syntax complexity and multi-line declaration patterns. Manually review public API declarations (class/struct/template) in each header file and verify they have preceding Doxygen /** @brief */ documentation blocks, or run a Doxygen build to check for undocumented-public-member warnings.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main focus: Edmonds-Blossom, weighted matching, Hungarian algorithm, LCA, and tree decompositions—matching the primary additions throughout the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch graph-round-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80e57ad345

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR significantly expands Aleph-w’s graph and tree algorithm offerings by adding general-graph matching/assignment algorithms and tree-query utilities, plus associated documentation, examples, and tests.

Changes:

  • Add general graph matching suite: Edmonds blossom (cardinality), weighted blossom (maximum weight), and a minimum-cost matching wrapper built on weighted blossom.
  • Add tree utilities and demos: LCA (binary lifting + Euler/RMQ) and tree decompositions (HLD + centroid), with new test coverage.
  • Add bridge-finding (Tarjan low-link) alongside cut nodes / biconnected components, plus various doc/style updates.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tpl_sort_utils.H Minor formatting/indentation adjustments in sorting utilities.
tpl_cut_nodes.H Expanded documentation and added Compute_Bridges/find_bridges implementation.
doxygen_groups.H Doxygen group list updated to mention matching and LCA.
ahFunctional.H Added in-place fill()/iota() algorithms and all_unique() helper.
ah-dry.H Small grammar fix in documentation.
ah-concepts.H Added AlephSequentialContainer / AlephSequentialIterableContainer concepts.
Tests/tree_decomposition_test.cc New tests for HLD and centroid decomposition across graph backends.
Tests/lca_test.cc New tests for LCA.H implementations (binary lifting and Euler+RMQ).
Tests/geom_algorithms_test_common.h Refactor/format helper utilities and comments.
Tests/cut_nodes_test.cc Added bridge-finding test suite.
Tests/ah_concepts_test.cc Added concept static_asserts and tests for fill() / iota().
Tests/CMakeLists.txt Registered new tests (incl. LCA and matching-related tests).
README.es.md Updated Spanish README with expanded algorithm listings and examples.
Min_Cost_Matching.H New minimum-cost (and perfect) matching API on general graphs.
Hungarian.H New Hungarian/Munkres assignment implementation and helpers.
Blossom_Weighted.H New maximum-weight matching API and wrapper around mwmatching backend.
Blossom.H New Edmonds blossom implementation for maximum cardinality matching.
Examples/blossom_example.cc New example program for cardinality blossom + TikZ export.
Examples/weighted_blossom_example.cc New example program for weighted blossom + TikZ export.
Examples/lca_example.cc New example program demonstrating LCA engines and parity/benchmarking.
Examples/hungarian_example.cc New example program for Hungarian assignment (min and max).
Examples/hld_example.cc New example program demonstrating HLD use-cases.
Examples/heavy_light_decomposition_example.cc New story-driven HLD demo using Tree_Decomposition.H.
Examples/centroid_decomposition_example.cc New story-driven centroid decomposition demo.
Examples/cut_nodes_example.C Updated example to include bridges demo and refreshed docs/CLI flags.
Examples/CMakeLists.txt Registered new example executables.
CMakeLists.txt Added new headers to build/install lists (matching, LCA/HLD, decompositions).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
ah-dry.H (5)

1701-1711: ⚠️ Potential issue | 🔴 Critical

items_ptr() — two type errors that prevent compilation on instantiation.

  1. Line 1704: C<Data> ret_val should be C<std::pair<Key, Data*>> — the declared return type and the container type disagree.
  2. Line 1708: p.second is Data, not Data* — it must be &p.second.
🐛 Proposed fix
  template <template <typename> class C = Aleph::DynList>
  C<std::pair<Key, Data*>> items_ptr() const
  {
-   C<Data> ret_val;
+   C<std::pair<Key, Data*>> ret_val;
    const_me()->for_each([&ret_val] (std::pair<Key, Data> & p)
                         {
-                          ret_val.append(std::pair<Key,Data*>(p.first,
-                                                              p.second));
+                          ret_val.append(std::pair<Key,Data*>(p.first,
+                                                              &p.second));
                         });
    return ret_val;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ah-dry.H` around lines 1701 - 1711, In items_ptr(), the local container is
declared with the wrong element type and the lambda appends a Data value instead
of a pointer: change the local variable ret_val to type C<std::pair<Key, Data*>>
(matching the function return type) and, inside the for_each lambda, append
std::pair<Key, Data*>(p.first, &p.second) (take the address of p.second) so the
stored second element is a Data*; keep the rest of items_ptr(),
const_me()->for_each and the lambda structure unchanged.

766-779: ⚠️ Potential issue | 🟠 Major

each(pos, slice, operation) — infinite loop when slice == 0.

When slice is 0 the inner for loop executes zero iterations, the outer while (true) never advances the iterator, and operation is called on the same element forever. The analogous take(i, j, step) at line 1449 already guards against step == 0; the same guard is needed here.

🐛 Proposed fix — guard against zero slice
  template <class Operation>
  void each(size_t pos, const size_t slice, Operation & operation) const
  {
+   if (slice == 0)
+     return;
    auto it = const_me()->get_it(pos);
-   while (true)
+   while (it.has_curr())
      {
        operation(it.get_curr());
        for (size_t k = 0; k < slice; ++k)
          {
            it.next();
            if (not it.has_curr())
              return;
          }
      }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ah-dry.H` around lines 766 - 779, The each(size_t pos, const size_t slice,
Operation & operation) method can infinite-loop when slice == 0 because the
inner for-loop never advances the iterator; add the same guard as in take(i, j,
step) to check slice == 0 at the start of each (or throw/assert) and handle it
(e.g. return immediately or raise an error). Locate the each function (uses
const_me()->get_it(pos), it.next(), it.has_curr()) and add a guard at the top to
prevent slice == 0 before entering the while(true) loop.

788-796: ⚠️ Potential issue | 🟡 Minor

mutable_for_each — missing Doxygen on primary overload.

The primary mutable_for_each(Operation&) overload has no documentation at all. As per coding guidelines, all public member functions must have @brief, @param, @return, and @throws Doxygen tags.

📝 Proposed doc addition
+  /** Traverse the container and perform a mutable operation on each element.
+   *
+   *  Unlike `for_each`, the operation receives a non-const `T&` reference,
+   *  allowing in-place mutation. Use with care on ordered containers (trees,
+   *  heaps, hash tables) as mutation may corrupt internal invariants.
+   *
+   *  `@param`[in] operation callable with signature `void(T&)`
+   *  `@throws` anything that `operation` throws
+   */
   template <class Operation>
   void mutable_for_each(Operation & operation)

As per coding guidelines: "All public member functions must have @brief, @param, @return, and @throws Doxygen tags."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ah-dry.H` around lines 788 - 796, Add a Doxygen comment block for the primary
template method mutable_for_each(Operation&), documenting its purpose and
behavior; include `@brief` describing that it applies a mutable operation to each
T element via me()->traverse, an `@param` for the Operation& (what it should
accept and that it may modify items), an `@return` (e.g., void) and an `@throws`
describing any exceptions that may propagate from the operation or traverse
calls; place the comment immediately above the mutable_for_each template
declaration so it covers the primary overload.

1302-1315: ⚠️ Potential issue | 🔴 Critical

partition(size_t n) — spurious template <class Operation> makes the overload uncallable.

Operation appears in neither the parameter list nor the function body, so it is non-deducible and has no default value. If any template argument remains neither deduced nor explicitly specified, compilation fails. Any call of the form container.partition(n) will fail deduction; the function is effectively unreachable without an ugly container.template partition<void>(n).

🐛 Proposed fix — remove the spurious template parameter
-  template <class Operation>
   std::pair<Aleph::DynList<T>, Aleph::DynList<T>> partition(size_t n) const
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ah-dry.H` around lines 1302 - 1315, The partition member function mistakenly
declares an unused template parameter Operation, making the overload
non-deducible; remove the spurious "template <class Operation>" so the signature
becomes a non-template member function partition(size_t n) const, leaving the
body (use of const_me(), ret_val, Aleph::DynList<T>, i, and the lambda)
unchanged; ensure no other callers rely on an explicit template parameter for
partition and rebuild to confirm the function is callable as
container.partition(n).

994-994: ⚠️ Potential issue | 🟡 Minor

Incorrect formula in foldl doc comment.

The pseudo-code at line 994 reads:

acc = op(acc, op(acc, item);

This has a nested op call and an unbalanced parenthesis. The actual implementation (line 1041) correctly applies a single call. The doc should read:

acc = op(acc, item);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ah-dry.H` at line 994, The doc comment for foldl contains an incorrect
pseudo-code line "acc = op(acc, op(acc, item);" — update the comment in the
foldl documentation (referencing the foldl function) to the correct expression
"acc = op(acc, item);" by removing the nested op call and fixing the unmatched
parenthesis so the doc matches the implementation.
Examples/cut_nodes_example.C (1)

373-381: ⚠️ Potential issue | 🟡 Minor

Off-by-one: paint_subgraphs() returns the next unused color.

Using that value as the component count and iterating <= num_colors overstates by one. Use num_colors - 1 for the count and loop while < next_color.

🛠️ Suggested fix
-  long num_colors = compute.paint_subgraphs();
+  long next_color = compute.paint_subgraphs();
+  const long num_colors = next_color - 1;
 ...
-  cout << "Number of components: " << num_colors << endl;
+  cout << "Number of components: " << num_colors << endl;
 ...
-  for (long color = 1; color <= num_colors; ++color)
+  for (long color = 1; color < next_color; ++color)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/cut_nodes_example.C` around lines 373 - 381, paint_subgraphs()
returns the next unused color, so using its return value as the component count
is off-by-one; update the code that reads long num_colors =
compute.paint_subgraphs() to treat num_colors as next_color (i.e., use
num_colors - 1 when displaying "Number of components") and change the loop to
iterate from color = 1 while color < num_colors (or equivalently color <=
num_colors - 1) when printing nodes per component; reference
compute.paint_subgraphs(), the num_colors variable, and the for loop that
currently uses <= num_colors.
Tests/geom_algorithms_test_common.h (1)

34-36: ⚠️ Potential issue | 🟡 Minor

Guard circumcenter_of against collinear triangles.

den is twice the signed area; for collinear points it becomes 0 and triggers division by zero. Please add a guard (prefer Aleph error macros) before the division.

🔧 Suggested fix
 `#include` <gtest/gtest.h>
 `#include` <geom_algorithms.H>
+#include <ah-errors.H>
 `#include` "geometry_visual_golden.h"
@@
     const Geom_Number d = ax * (by - cy) + bx * (cy - ay) + cx * (ay - by);
     const Geom_Number den = d + d;
+    ah_domain_error_if(den == 0)
+      << "circumcenter_of: collinear points";
 
     return {

Also applies to: 155-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/geom_algorithms_test_common.h` around lines 34 - 36, The function
circumcenter_of computes a denominator den (twice the signed area) and currently
divides by it unguarded; add a check in circumcenter_of immediately before the
division that tests if fabs(den) (or den == 0) is below a small epsilon and, if
so, raise an Aleph error macro (e.g. ALEPH_ERROR or the project’s standard error
macro) with a clear message like "collinear triangle in circumcenter_of" to
prevent the division-by-zero; apply the same guard to the other occurrence of
the same code block (lines ~155-174) so both divisions are protected.
🟡 Minor comments (12)
Min_Cost_Matching.H-241-259 (1)

241-259: ⚠️ Potential issue | 🟡 Minor

Add required Doxygen tags to public aliases/wrappers.

The blossom_minimum_cost_matching() alias and related wrappers omit @param/@return/@throws tags, which violates the public‑API documentation rules. As per coding guidelines: "All public member functions must have @brief, @param, @return, and @throws Doxygen tags" and "Doxygen must generate no warnings for changed files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Min_Cost_Matching.H` around lines 241 - 259, Add missing Doxygen tags to the
public alias blossom_minimum_cost_matching so it documents parameters, return
value, and possible exceptions: add `@param` entries for g (graph reference),
matching (DynDlist of Arc* to receive the matching), cost (cost functor), sa
(show-arc functor), and max_cardinality (bool), an `@return` describing the
Min_Cost_Matching_Result<long long> returned, and an `@throws` describing any
exceptions that may propagate from compute_minimum_cost_general_matching (e.g.,
allocation or algorithm errors); keep the existing `@brief` and mention that this
function forwards to compute_minimum_cost_general_matching in the description.
Hungarian.H-361-431 (1)

361-431: ⚠️ Potential issue | 🟡 Minor

Add required @throws tags to public accessors.

Public methods like get_total_cost(), get_assignments(), get_row_assignments(), dimension(), rows(), and cols() are missing @throws tags. Please add them (even if “none”) to satisfy Doxygen requirements. As per coding guidelines: "All public member functions must have @brief, @param, @return, and @throws Doxygen tags" and "Doxygen must generate no warnings for changed files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Hungarian.H` around lines 361 - 431, The Doxygen warnings come from missing
`@throws` tags on the public accessors; add a `@throws` section (use "none" if they
cannot throw) to each affected function comment block—specifically update
get_total_cost(), get_assignments(), get_row_assignments(),
get_col_assignments(), dimension(), rows(), and cols() to include an `@throws` tag
(e.g., "@throws none") so all public member functions have `@brief`, `@param`,
`@return`, and `@throws` tags and Doxygen emits no warnings.
Tree_Decomposition.H-571-719 (1)

571-719: ⚠️ Potential issue | 🟡 Minor

Add full Doxygen tags for public accessors.

Public methods like size(), root(), node_of(), parent_id(), depth_of_id(), and distance_id() are missing required @param/@return/@throws tags. Please update these to satisfy documentation rules. As per coding guidelines: "All public member functions must have @brief, @param, @return, and @throws Doxygen tags" and "Doxygen must generate no warnings for changed files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tree_Decomposition.H` around lines 571 - 719, Add full Doxygen comments for
all public accessors (e.g., size, is_empty, root, root_id, node_of, id_of,
parent_id, parent_of, depth_of_id, depth_of, subtree_size_of_id,
subtree_size_of, heavy_child_id, heavy_child, head_id, head_of, position_of_id,
position_of, subtree_range_id, subtree_range, is_ancestor_id, is_ancestor,
lca_id, lca, distance_id, distance) including `@brief`, `@param` for each input
parameter, `@return` describing the result, and `@throws` where the method can
validate or throw (e.g., calls to topology_.validate_id or ensure_not_empty);
ensure descriptions mention special return values like NONE mapping to nullptr
where applicable; follow existing Doxygen style in the file and run the Doxygen
generator to confirm zero warnings.
HLD.H-600-623 (1)

600-623: ⚠️ Potential issue | 🟡 Minor

Complete Doxygen tags for public accessors.

Accessors like size(), is_empty(), root(), root_id(), node_of(), and id_of() lack the required @param/@return/@throws tags, which will trigger Doxygen warnings for a public header. Please update these to include the full tag set. As per coding guidelines: "All public member functions must have @brief, @param, @return, and @throws Doxygen tags" and "Doxygen must generate no warnings for changed files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HLD.H` around lines 600 - 623, Add full Doxygen tags to each public accessor:
for size(), is_empty(), root(), root_id(), node_of(const size_t), and
id_of(const Node*). Keep the existing `@brief` text and add an `@return` describing
the returned value for every function; add `@param` for node_of (id) and id_of
(node) describing the parameter; and add an `@throws` tag for each (use "None" or
"No exceptions thrown" since all are noexcept). Ensure tags are placed in the
same comment block above each method (e.g., above size(), node_of(), id_of(),
root(), root_id(), is_empty()) so Doxygen warnings are eliminated.
blossom_weighted_mwmatching.H-1908-1910 (1)

1908-1910: ⚠️ Potential issue | 🟡 Minor

Replace ! with not per style rules.

This header uses the ! operator instead of the keyword not. The file contains 2 instances that need correction:

  • Line 1908: while (! stack.is_empty())
  • Line 2200: ah_invalid_argument_if(! std::isfinite(edge.weight))

Per coding guidelines: "Use and, or, not for logical operations (instead of &&, ||, !)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blossom_weighted_mwmatching.H` around lines 1908 - 1910, Replace the use of
the C-style negation operator with the keyword form per style rules: change
occurrences that negate boolean expressions to use "not" instead of "!" —
specifically update the loop condition in the method containing "while (!
stack.is_empty())" to "while (not stack.is_empty())" and update the check inside
ah_invalid_argument_if from "ah_invalid_argument_if(!
std::isfinite(edge.weight))" to "ah_invalid_argument_if(not
std::isfinite(edge.weight))"; ensure spacing and parentheses remain correct
around the expressions.
Examples/hungarian_example.cc-58-63 (1)

58-63: ⚠️ Potential issue | 🟡 Minor

Bug: prints total cost instead of individual assignment cost.

ha.get_total_cost() returns the overall optimal cost, not the cost of a single assignment. Each line in this loop will print the same value (the total), which is misleading. The "Detailed" section below (lines 72–76) correctly uses costs[r][c] for per-assignment costs. This first loop should do the same, or simply omit the cost column here since the detailed section follows immediately.

Proposed fix
   cout << "Assignments:" << endl;
   for (auto [r, c] : ha.get_assignments())
     cout << "  Worker " << r << " -> Task " << c
-         << " (cost " << ha.get_total_cost() << ")" << endl;
+         << endl;
   cout << endl;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/hungarian_example.cc` around lines 58 - 63, The loop printing
assignments uses ha.get_total_cost() for each row which is incorrect; change the
per-assignment cost to the matrix value costs[r][c] (the same approach used
later in the "Detailed" section) so the cout inside the for (auto [r, c] :
ha.get_assignments()) prints costs[r][c] instead of ha.get_total_cost(); ensure
the variable costs is in scope where get_assignments() is iterated (or omit the
per-assignment cost column if you prefer).
README.md-921-926 (1)

921-926: ⚠️ Potential issue | 🟡 Minor

Apply the flagged style nits (readability).

  • The “Problem patterns solved” bullets start with “Dynamic …” multiple times (LanguageTool repetition warning).
  • “full service area” should be “full-service area” if used as a compound adjective.
Proposed tweaks
- - **Dynamic path aggregates**: `sum/min/max/xor` on `path(u, v)` with node point updates.
- - **Dynamic subtree aggregates**: full subtree query `subtree(u)` with node point updates.
- - **Dynamic nearest active node** (unweighted distance in edges): mark nodes as active and query `min dist(u, active)`.
- - **Dynamic farthest/threshold-style distance queries** by scanning centroid ancestor chains.
+ - **Online path aggregates**: `sum/min/max/xor` on `path(u, v)` with node point updates.
+ - **Online subtree aggregates**: full subtree query `subtree(u)` with node point updates.
+ - **Nearest active node queries** (unweighted distance in edges): mark nodes as active and query `min dist(u, active)`.
+ - **Farthest/threshold-style distance queries** by scanning centroid ancestor chains.
@@
-Recipe 2: Subtree aggregate (full service area)
+Recipe 2: Subtree aggregate (full-service area)

Also applies to: 970-986

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 921 - 926, Revise the "Problem patterns solved:"
bullet list (the header and bullets containing "Dynamic path aggregates",
"Dynamic subtree aggregates", "Dynamic nearest active node", and the centroid
chain note) to remove repeated sentence starts and improve readability—e.g.,
combine/variate phrasing so not every bullet begins with "Dynamic", reword "full
subtree query `subtree(u)`" to use "full-service area" only if appropriate but
otherwise hyphenate "full-service area" when used as a compound adjective, and
apply the same style edits to the other occurrence of this list (the block
around lines 970-986). Ensure you keep the technical names and inline code
examples (`path(u, v)`, `subtree(u)`) unchanged while only adjusting surrounding
wording for clarity and hyphenation.
README.es.md-371-393 (1)

371-393: ⚠️ Potential issue | 🟡 Minor

Remove unnecessary escaping in prose and apply the flagged spacing fixes.

There are occurrences of \"...\" in normal markdown text (not code). Markdown doesn’t require escaping quotes there, and LanguageTool flagged spacing around those sequences.

Proposed fix (representative lines)
-  Ejemplo: \"suma de riesgo entre sucursal A y B\" muchas veces.
+  Ejemplo: "suma de riesgo entre sucursal A y B" muchas veces.
@@
-  Ejemplo: \"costo total de una región operativa\" donde cambian nodos individuales.
+  Ejemplo: "costo total de una región operativa" donde cambian nodos individuales.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.es.md` around lines 371 - 393, Remove unnecessary backslash-escaped
double quotes and correct spacing in the prose sections (not code) of the
README: replace occurrences like \"suma de riesgo entre sucursal A y B\" with
straight inline quotes "suma de riesgo entre sucursal A y B", and similarly for
other quoted phrases; also fix any extra or missing spaces around those quotes
flagged by LanguageTool. Ensure you only change plain markdown paragraphs
(references to Tree_Decomposition.H, Gen_HLD_Path_Query,
Gen_Centroid_Decomposition, LCA.H, tpl_mo_on_trees.H) and do not alter code
blocks or inline code identifiers.
Tests/blossom_test.cc-399-432 (1)

399-432: ⚠️ Potential issue | 🟡 Minor

Tighten the bipartite cross-check assertion.

In AgreesWithHopcroftKarpOnBipartiteGraphs, you compare blossom_size against hk_matching.size(), but don’t explicitly assert blossom_matching.size() == blossom_size (helpful if the API ever regresses and returns a mismatched cardinality vs list size).

Proposed fix
           const size_t blossom_size =
               blossom_maximum_cardinality_matching<Graph>(g, blossom_matching);
           hopcroft_karp_matching<Graph>(g, hk_matching);
 
+          EXPECT_EQ(blossom_matching.size(), blossom_size)
+            << "left=" << left << " right=" << right << " sample=" << sample;
           EXPECT_EQ(blossom_size, hk_matching.size())
             << "left=" << left << " right=" << right << " sample=" << sample;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/blossom_test.cc` around lines 399 - 432, Add an explicit assertion that
the returned cardinality blossom_size matches the actual list size of
blossom_matching: after calling blossom_maximum_cardinality_matching<Graph>(g,
blossom_matching) add EXPECT_EQ(blossom_size, blossom_matching.size()) (include
the same contextual failure message with left/right/sample as used for the hk
check) so we verify the function’s return value and the list length are
consistent; keep the existing verify_matching checks and variable names
(blossom_size, blossom_matching, hk_matching,
blossom_maximum_cardinality_matching, hopcroft_karp_matching).
Examples/weighted_blossom_example.cc-100-114 (1)

100-114: ⚠️ Potential issue | 🟡 Minor

Guard Scenario.edges indices in build_graph().

build_graph() assumes every (u, v) in s.edges is in-range and will index nodes[u]/nodes[v] directly (Line 110-112). This is fine for the current hardcoded scenarios, but it becomes a footgun when someone edits/extends scenarios.

Proposed fix
     for (const auto & [u, v, w] : s.edges)
-      g.insert_arc(nodes[u], nodes[v], w);
+      {
+        if (u >= s.nodes.size() or v >= s.nodes.size() or u == v)
+          continue;
+        g.insert_arc(nodes[u], nodes[v], w);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/weighted_blossom_example.cc` around lines 100 - 114, In
build_graph(), validate each edge's endpoint indices before indexing nodes:
check that u and v are within [0, nodes.size()) and only call
g.insert_arc(nodes[u], nodes[v], w) when both are valid; for invalid indices
either skip the edge (and optionally log a warning) or throw a clear exception;
update the loop that processes s.edges (referencing Scenario::edges and the use
of nodes[u]/nodes[v] and GT::Node pointers) to perform this guard.
README.md-2088-2108 (1)

2088-2108: ⚠️ Potential issue | 🟡 Minor

Fix undefined nodes[2] in the bridges example snippet.

The snippet uses cb.find_bridges(nodes[2]) (Line 2107), but nodes isn’t defined in the example, so readers can’t paste/compile it as-is.

One minimal doc-only fix
-    auto b2 = cb.find_bridges(nodes[2]); // start from a specific node
+    // Optional: start from a specific node pointer you already have in hand.
+    // (Define `start` when you build the graph.)
+    auto b2 = cb.find_bridges(start);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 2088 - 2108, The example calls
cb.find_bridges(nodes[2]) but never defines nodes; fix by introducing a nodes
collection from the graph before that call (e.g. obtain the graph's node list
via g.get_nodes() or equivalent) and then call
Compute_Bridges<Graph>::find_bridges with that node (nodes[2]); ensure the
README snippet includes the nodes definition so cb.find_bridges(nodes[2])
compiles.
README.es.md-85-151 (1)

85-151: ⚠️ Potential issue | 🟡 Minor

Fix markdownlint MD040: add languages to fenced code blocks.

markdownlint reports missing language specifiers for fenced blocks (e.g., the ASCII tables). Use text for diagrams.

Proposed fix
-```
+```text
 ┌────────────────────────────────────────────────────────────────────────────┐
 │                  RESUMEN DE ESTRUCTURAS DE DATOS                           │
 ...
 └────────────────────────────────────────────────────────────────────────────┘

@@
- +text
┌────────────────────────────────────────────────────────────────────────────┐
│ RESUMEN DE ALGORITMOS │
...
└────────────────────────────────────────────────────────────────────────────┘

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.es.md` around lines 85 - 151, The fenced ASCII-art code blocks for
"RESUMEN DE ESTRUCTURAS DE DATOS" and "RESUMEN DE ALGORITMOS" are missing
language specifiers; update each opening triple-backtick (```) for those two
ASCII diagrams (the blocks that start with the box drawing ┌ and headings
"RESUMEN DE ESTRUCTURAS DE DATOS" and "RESUMEN DE ALGORITMOS") to include the
language tag "text" (i.e., change ``` to ```text) so markdownlint MD040 is
satisfied.
🧹 Nitpick comments (4)
Examples/heavy_light_decomposition_example.cc (1)

143-210: Prefer std::format over printf in C++20 examples.

The example uses std::printf throughout; the project style favors std::format for type‑safe formatting. Consider switching to std::format + std::cout (apply similarly to all print calls).

🔧 Example pattern
+# include <format>
@@
-  std::printf("  id=%zu  %-8s  pos=%zu\n", i, labels[i], hld.position_of_id(i));
+  std::cout << std::format("  id={}  {:<8}  pos={}\n", i, labels[i], hld.position_of_id(i));
As per coding guidelines: "Use std::format for type-safe string formatting (prefer over printf/streams)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/heavy_light_decomposition_example.cc` around lines 143 - 210,
Replace all uses of std::printf in this example with C++20 type-safe formatting
using std::format combined with std::cout (or std::puts if you want a
newline-only). Specifically, update the prints around
hld.size()/labels/hld.position_of_id, the "Path maintenance cost queries" loop
that prints q.name/ans/brute, the subtree totals loop that prints
labels[id]/ans, the budget change messages, the "North -> S-A2 after update"
print that follows grid.update_node_id/grid.set_node_id, and the
hld.for_each_path_segment print; include <format> and construct formatted
strings via std::format(...) then output with std::cout << formatted << '\n' to
replace each std::printf call. Ensure types/format specifiers are adjusted to
std::format syntax (use {} placeholders) and keep existing variables like
id_sa2/id_west, na/sa2, grid.query_path_id, brute_path_sum unchanged.
Tests/hld_test.cc (1)

66-100: Consider extracting shared test helpers to reduce duplication.

build_graph_with_unit_arcs, Positive_Arc_Filter, and make_random_tree_edges are duplicated across lca_test.cc, hld_test.cc, and (per the snippets) tree_decomposition_test.cc. Extracting them into a shared test utility header would reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/hld_test.cc` around lines 66 - 100, The duplicated test helpers
build_graph_with_unit_arcs, Positive_Arc_Filter, and make_random_tree_edges
appear in multiple test files; extract these into a single shared test utility
header (e.g., tests/utilities/test_graph_helpers.hpp) and replace the local
copies by including that header in lca_test.cc, hld_test.cc, and
tree_decomposition_test.cc. Move the template function
build_graph_with_unit_arcs, the struct Positive_Arc_Filter, and the
make_random_tree_edges implementation into the new header within the Aleph (or
anonymous) test namespace, ensure all required headers/types are included or
forward-declared there, and update the test files to remove the duplicates and
add a single `#include` for the new header. Ensure symbol names
(build_graph_with_unit_arcs, Positive_Arc_Filter, make_random_tree_edges) and
their signatures remain unchanged so existing tests compile without other
changes.
Examples/weighted_blossom_example.cc (1)

55-64: Consider Aleph containers (and avoid using namespace std;) for guideline compliance.

This example leans heavily on std::vector/std::set/std::tuple and using namespace std;. If examples are meant to model “Aleph-first” style, consider switching to Aleph containers (or documenting why STL is preferred here, e.g., iostream/TikZ convenience).

As per coding guidelines: “MUST use Aleph containers when an equivalent exists. Standard library containers are ONLY permitted when no Aleph equivalent is available.”

Also applies to: 83-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/weighted_blossom_example.cc` around lines 55 - 64, The example
currently uses std::vector, std::set, std::tuple (and a global "using namespace
std;"); replace those STL container usages with the corresponding Aleph
container types and headers wherever an equivalent exists and remove the "using
namespace std;" directive; update all occurrences of std::vector, std::set,
std::tuple (and any unqualified container names that relied on the using
directive) to the Aleph equivalents (and include the appropriate Aleph headers)
so the file conforms to the "MUST use Aleph containers" guideline while keeping
STL only for types with no Aleph counterpart.
Tests/blossom_test.cc (1)

48-56: Use C++20 std::countr_zero from <bit> instead of __builtin_ctzll.

__builtin_ctzll at lines 174 and 182 is a GCC/Clang builtin. Since the repo targets C++20, std::countr_zero is the portable standard library equivalent and aligns with the C++20 coding guidelines. Both calls are safe here (guaranteed non-zero preconditions via the function's early exit and loop condition), so the replacement is semantically correct.

Proposed fix
 # include <algorithm>
+# include <bit>
 # include <cstdint>
@@
-      const size_t i = static_cast<size_t>(__builtin_ctzll(mask));
+      const size_t i = static_cast<size_t>(std::countr_zero(mask));
@@
-          const size_t j = static_cast<size_t>(__builtin_ctzll(options));
+          const size_t j = static_cast<size_t>(std::countr_zero(options));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/blossom_test.cc` around lines 48 - 56, Replace the GCC/Clang builtin
uses of __builtin_ctzll with the C++20 standard std::countr_zero and add
`#include` <bit>; specifically, update the two call sites that currently call
__builtin_ctzll(...) to std::countr_zero(...) (ensuring the argument type is an
unsigned integer type compatible with std::countr_zero, e.g.,
static_cast<unsigned long long>(... ) if necessary) and add the <bit> include at
the top of Tests/blossom_test.cc so the code uses the portable C++20 API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ahFunctional.H`:
- Around line 2442-2462: Add Doxygen `@overload` documentation for the newly added
templated overloads of all_unique (the rvalue-ref/Default-Equal overload:
template <AlephSequentialIterableContainer Container, class Equal =
std::equal_to<typename Container::Item_Type>> inline bool all_unique(const
Container & container, Equal && eq = Equal())) by adding a Doxygen block that
documents parameters, return value and marks it as `@overload` of the primary
all_unique(const Container&, Equal&), and implement a unit test that calls the
new overload with a non-trivial equality predicate (e.g., a case-insensitive
comparator for strings or a floating-point comparator with tolerance) to ensure
the overload is exercised and behavior verified; update test names to reference
all_unique overload and assert both true/false scenarios.

In `@blossom_weighted_mwmatching.H`:
- Around line 2175-2214: The safe minimum weight initialization uses
numeric_limits::min(), which for floating types is the smallest positive value
and will misflag negative weights; change the min_safe_weight calculation in the
function that declares min_safe_weight and max_safe_weight so that for
floating-point WeightType you use std::numeric_limits<WeightType>::lowest()/4
(or otherwise select lowest() when !std::numeric_limits<WeightType>::is_integer)
while keeping max_safe_weight = std::numeric_limits<WeightType>::max()/4; update
the min_safe_weight binding (and any related comments) so the later check
ah_invalid_argument_if(min_weight < min_safe_weight ...) behaves correctly for
floats.

In `@Examples/centroid_decomposition_example.cc`:
- Around line 45-51: Add a direct include for <algorithm> to this translation
unit so uses of std::min in Examples/centroid_decomposition_example.cc resolve
reliably (currently relying on transitive includes). Edit the top-of-file
includes (near the existing `#include` <Tree_Decomposition.H> and `#include`
<tpl_graph.H>) to add `#include` <algorithm>, ensuring the calls to std::min (used
around the centroid decomposition logic / main or related functions) compile on
all toolchains.
- Around line 93-110: The example assumes centroid/HLD ids equal the “Village-X”
indices; instead, after constructing Centroid_Decomposition<G> cd and
Heavy_Light_Decomposition<G> hld, build a mapping using cd.id_of(node) (e.g.,
create label_by_id and node_by_id arrays keyed by cd.id_of(node)) and then
replace direct uses of label[c], active(u) and literal id calls like
for_each_centroid_ancestor_id(10,...) with lookups via
label_by_id[cd.id_of(node)] or using node_by_id to pass correct node ids to hld
and for_each_centroid_ancestor_id so all printing, activation and oracle
distance assertions use the centroid/HLD id mapping rather than assuming 0..n
order.

In `@Examples/weighted_blossom_example.cc`:
- Around line 178-223: The TikZ output in write_tikz() currently emits edge
draws and edge labels (using node names like (nX) and $(nX)!0.5!(nY)$ from
s.edges and matched_set) before emitting the vertex node definitions from
s.nodes, which causes unknown-node errors; fix by writing the loop that emits
the "\\node[vertex] (n...)" definitions before the loop that emits edge "\\draw"
and wlabel nodes, and update the "edge" style in the \\tikzset (used in the
existing write_tikz()) to include shortening options (e.g., shorten <= and
shorten >=) so edges stop at vertex borders rather than drawing through nodes.

In `@Hungarian.H`:
- Around line 92-100: Add a compile-time guard that prevents using unsigned
types for Cost_Type by including <type_traits> and adding a static_assert near
the template/class declaration that defines Cost_Type (e.g., the
template<typename Cost_Type> for the Hungarian implementation) asserting
std::is_signed<std::remove_cv_t<Cost_Type>>::value ||
std::is_floating_point<std::remove_cv_t<Cost_Type>>::value, with a clear error
message like "Cost_Type must be signed or floating-point to avoid underflow when
negating costs"; apply the same static_assert at the other template declaration
referenced around lines 169-176.
- Around line 487-496: The negation can overflow for signed integral Cost_Type
(negating numeric_limits<Cost_Type>::min()), so update hungarian_max_assignment
to detect when Cost_Type is a signed integer and, in that case, promote values
to a wider signed integer type before negation: compute a Promoted =
std::make_signed_t<std::common_type_t<Cost_Type, long long>> (or similar),
allocate DynMatrix<Promoted> negated, fill with
-static_cast<Promoted>(cost.read(i,j)), call hungarian_assignment on that
matrix, then convert/negate result.total_cost back to Cost_Type (or the
caller-expected type) safely; if promotion isn't feasible, alternatively detect
presence of numeric_limits<Cost_Type>::min() and apply a uniform offset to all
entries prior to negation and adjust result.total_cost accordingly. Ensure
changes reference negated, Cost_Type, hungarian_assignment, and
result.total_cost.

In `@Min_Cost_Matching.H`:
- Around line 193-202: compute_minimum_cost_general_matching currently assumes
the graph is undirected but does not validate that, leading to undefined
behavior; add the same undirected-graph guard used by the perfect-matching
variant at the start of compute_minimum_cost_general_matching (check GT's
undirected/directed query method—e.g.
is_directed()/directed()/is_undirected()—and if the graph is directed, return an
error/throw std::invalid_argument or return an appropriate
Min_Cost_Matching_Result<long long> error state), so the blossom backend is
never invoked on a directed GT and behavior matches the
compute_minimum_cost_perfect_matching contract.
- Around line 119-137: to_ll_checked currently casts LLONG_MAX/MIN into T which
can narrow incorrectly; instead determine if T can exceed long long using
std::numeric_limits<T>::digits and only perform bounds checks when necessary,
and when checking convert value to a wider type (e.g. __int128) to compare
against LLONG_MAX/LLONG_MIN to avoid narrowing. Update the template function to:
use constexpr logic on std::numeric_limits<T>::digits and std::is_signed_v<T> to
decide whether an upper/lower check is required, and when required compare
static_cast<__int128>(value) to static_cast<__int128>(std::numeric_limits<long
long>::max()/min()) and trigger ah_overflow_error_if with the same message;
finally return static_cast<long long>(value) as before.

In `@Tests/cut_nodes_test.cc`:
- Around line 939-946: The new bridge tests currently use std::set and
std::vector which violates the Aleph-container rule; replace the std::set in the
helper bridge_set with DynSetTree<Graph::Arc*> and use Aleph list/array types
(DynList<Graph::Arc*> or DynArray<Graph::Arc*>) for the arc collections
referenced in the tests (including the other occurrences around the second block
noted at 1162-1164). Update the bridge_set signature and implementation to build
and return a DynSetTree<Graph::Arc*> by iterating the incoming
DynList<Graph::Arc*> (using the existing Iterator usage pattern), and replace
any std::vector usages in the tests with DynList or DynArray to preserve
semantics.

In `@Tests/min_cost_matching_test.cc`:
- Around line 46-58: Replace compiler-specific __builtin_ctzll calls with the
C++20 std::countr_zero for portability: add `#include` <bit> if not present and
change __builtin_ctzll(mask) and __builtin_ctzll(options) to
static_cast<int>(std::countr_zero(mask)) and
static_cast<int>(std::countr_zero(options)) (or equivalent casts used elsewhere)
in the DP oracle code (the mask/options usages), and make the identical
replacements in the other test file where __builtin_ctzll is used
(blossom_test.cc).

In `@tpl_cut_nodes.H`:
- Around line 716-749: Move the empty-graph check before validating start (so
find_bridges() returns immediately on gptr->get_num_nodes()==0 without error)
and change the DFS invocation to cover all components: after initializing
NODE_COUNTER/ NODE_BITS and resetting arcs, run __dfs(start, nullptr, curr_df,
bridges) for the provided start but then iterate gptr->for_each_node and for any
node with NODE_COUNTER==0 invoke __dfs(node, nullptr, curr_df, bridges) to
explore disconnected components; keep clearing NODE_COOKIE afterward. Use the
existing symbols find_bridges, __dfs, NODE_COUNTER, NODE_BITS, NODE_COOKIE, and
gptr->for_each_node to locate and implement the changes.

---

Outside diff comments:
In `@ah-dry.H`:
- Around line 1701-1711: In items_ptr(), the local container is declared with
the wrong element type and the lambda appends a Data value instead of a pointer:
change the local variable ret_val to type C<std::pair<Key, Data*>> (matching the
function return type) and, inside the for_each lambda, append std::pair<Key,
Data*>(p.first, &p.second) (take the address of p.second) so the stored second
element is a Data*; keep the rest of items_ptr(), const_me()->for_each and the
lambda structure unchanged.
- Around line 766-779: The each(size_t pos, const size_t slice, Operation &
operation) method can infinite-loop when slice == 0 because the inner for-loop
never advances the iterator; add the same guard as in take(i, j, step) to check
slice == 0 at the start of each (or throw/assert) and handle it (e.g. return
immediately or raise an error). Locate the each function (uses
const_me()->get_it(pos), it.next(), it.has_curr()) and add a guard at the top to
prevent slice == 0 before entering the while(true) loop.
- Around line 788-796: Add a Doxygen comment block for the primary template
method mutable_for_each(Operation&), documenting its purpose and behavior;
include `@brief` describing that it applies a mutable operation to each T element
via me()->traverse, an `@param` for the Operation& (what it should accept and that
it may modify items), an `@return` (e.g., void) and an `@throws` describing any
exceptions that may propagate from the operation or traverse calls; place the
comment immediately above the mutable_for_each template declaration so it covers
the primary overload.
- Around line 1302-1315: The partition member function mistakenly declares an
unused template parameter Operation, making the overload non-deducible; remove
the spurious "template <class Operation>" so the signature becomes a
non-template member function partition(size_t n) const, leaving the body (use of
const_me(), ret_val, Aleph::DynList<T>, i, and the lambda) unchanged; ensure no
other callers rely on an explicit template parameter for partition and rebuild
to confirm the function is callable as container.partition(n).
- Line 994: The doc comment for foldl contains an incorrect pseudo-code line
"acc = op(acc, op(acc, item);" — update the comment in the foldl documentation
(referencing the foldl function) to the correct expression "acc = op(acc,
item);" by removing the nested op call and fixing the unmatched parenthesis so
the doc matches the implementation.

In `@Examples/cut_nodes_example.C`:
- Around line 373-381: paint_subgraphs() returns the next unused color, so using
its return value as the component count is off-by-one; update the code that
reads long num_colors = compute.paint_subgraphs() to treat num_colors as
next_color (i.e., use num_colors - 1 when displaying "Number of components") and
change the loop to iterate from color = 1 while color < num_colors (or
equivalently color <= num_colors - 1) when printing nodes per component;
reference compute.paint_subgraphs(), the num_colors variable, and the for loop
that currently uses <= num_colors.

In `@Tests/geom_algorithms_test_common.h`:
- Around line 34-36: The function circumcenter_of computes a denominator den
(twice the signed area) and currently divides by it unguarded; add a check in
circumcenter_of immediately before the division that tests if fabs(den) (or den
== 0) is below a small epsilon and, if so, raise an Aleph error macro (e.g.
ALEPH_ERROR or the project’s standard error macro) with a clear message like
"collinear triangle in circumcenter_of" to prevent the division-by-zero; apply
the same guard to the other occurrence of the same code block (lines ~155-174)
so both divisions are protected.

---

Nitpick comments:
In `@Examples/heavy_light_decomposition_example.cc`:
- Around line 143-210: Replace all uses of std::printf in this example with
C++20 type-safe formatting using std::format combined with std::cout (or
std::puts if you want a newline-only). Specifically, update the prints around
hld.size()/labels/hld.position_of_id, the "Path maintenance cost queries" loop
that prints q.name/ans/brute, the subtree totals loop that prints
labels[id]/ans, the budget change messages, the "North -> S-A2 after update"
print that follows grid.update_node_id/grid.set_node_id, and the
hld.for_each_path_segment print; include <format> and construct formatted
strings via std::format(...) then output with std::cout << formatted << '\n' to
replace each std::printf call. Ensure types/format specifiers are adjusted to
std::format syntax (use {} placeholders) and keep existing variables like
id_sa2/id_west, na/sa2, grid.query_path_id, brute_path_sum unchanged.

In `@Examples/weighted_blossom_example.cc`:
- Around line 55-64: The example currently uses std::vector, std::set,
std::tuple (and a global "using namespace std;"); replace those STL container
usages with the corresponding Aleph container types and headers wherever an
equivalent exists and remove the "using namespace std;" directive; update all
occurrences of std::vector, std::set, std::tuple (and any unqualified container
names that relied on the using directive) to the Aleph equivalents (and include
the appropriate Aleph headers) so the file conforms to the "MUST use Aleph
containers" guideline while keeping STL only for types with no Aleph
counterpart.

In `@Tests/blossom_test.cc`:
- Around line 48-56: Replace the GCC/Clang builtin uses of __builtin_ctzll with
the C++20 standard std::countr_zero and add `#include` <bit>; specifically, update
the two call sites that currently call __builtin_ctzll(...) to
std::countr_zero(...) (ensuring the argument type is an unsigned integer type
compatible with std::countr_zero, e.g., static_cast<unsigned long long>(... ) if
necessary) and add the <bit> include at the top of Tests/blossom_test.cc so the
code uses the portable C++20 API.

In `@Tests/hld_test.cc`:
- Around line 66-100: The duplicated test helpers build_graph_with_unit_arcs,
Positive_Arc_Filter, and make_random_tree_edges appear in multiple test files;
extract these into a single shared test utility header (e.g.,
tests/utilities/test_graph_helpers.hpp) and replace the local copies by
including that header in lca_test.cc, hld_test.cc, and
tree_decomposition_test.cc. Move the template function
build_graph_with_unit_arcs, the struct Positive_Arc_Filter, and the
make_random_tree_edges implementation into the new header within the Aleph (or
anonymous) test namespace, ensure all required headers/types are included or
forward-declared there, and update the test files to remove the duplicates and
add a single `#include` for the new header. Ensure symbol names
(build_graph_with_unit_arcs, Positive_Arc_Filter, make_random_tree_edges) and
their signatures remain unchanged so existing tests compile without other
changes.

@lrleon lrleon changed the title Graph round 3 Add Edmonds-Blossom, weighted matching, Hungarian algorithm, LCA, and tree decomposition implementations Feb 22, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CMakeLists.txt (1)

82-126: ⚠️ Potential issue | 🔴 Critical

Dominators.H and Hungarian.H are missing from the installed header list.

Both Dominators.H and Hungarian.H exist in the repository and are new public headers with full APIs, but they are not listed in HLIST. They will not be installed via install(FILES ${HLIST} ...), making these APIs unavailable to downstream consumers of the library.

Proposed fix
     LCA.H
     HLD.H
+    Dominators.H
+    Hungarian.H
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CMakeLists.txt` around lines 82 - 126, HLIST is missing the new public
headers Dominators.H and Hungarian.H so they aren't installed; add
"Dominators.H" and "Hungarian.H" to the HLIST variable (the list used by
install(FILES ${HLIST} ...)) so those headers are packaged/installed for
downstream consumers, ensuring you insert the names alongside the other .H
entries in the HLIST definition.
🟡 Minor comments (10)
Tests/ah-functional.cc-1490-1491 (1)

1490-1491: ⚠️ Potential issue | 🟡 Minor

tolower on plain char is undefined behavior for non-ASCII values.

char is signed on most platforms; passing a negative value to tolower is UB per C/C++ standard. The tests only use ASCII strings today, but the struct is reusable. Cast to unsigned char before calling tolower.

🐛 Proposed fix
-      if (tolower(a[i]) != tolower(b[i])) return false;
+      if (tolower(static_cast<unsigned char>(a[i])) !=
+          tolower(static_cast<unsigned char>(b[i]))) return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ah-functional.cc` around lines 1490 - 1491, The loop compares
characters using tolower on plain char (variables a and b) which is undefined
for negative values; change the calls to cast each character to unsigned char
before calling tolower — i.e., replace tolower(a[i]) and tolower(b[i]) with
tolower((unsigned char)a[i]) and tolower((unsigned char)b[i]) in the loop that
iterates over a.size() so the comparison uses defined behavior.
.github/workflows/planarity_gephi_nightly.yml-504-512 (1)

504-512: ⚠️ Potential issue | 🟡 Minor

Webhook secret is passed as a command-line argument — visible in process listings.

${ALERT_WEBHOOK} (sourced from a secret) is passed via --webhook-url CLI flag. On multi-tenant runners, CLI arguments are visible in /proc/*/cmdline or ps aux. The secret is already available as an env var ALERT_WEBHOOK; have the Ruby script read it directly from the environment instead.

🛡️ Proposed fix
          ruby scripts/planarity_gephi_regression_notify.rb \
            --report-json build-gephi-nightly-summary/gephi_nightly_comparison.json \
            --output-md build-gephi-nightly-summary/gephi_nightly_alert.md \
            --run-url "${RUN_URL}" \
            --repository "${REPOSITORY}" \
-           --webhook-url "${ALERT_WEBHOOK}" \
+           --webhook-url-env ALERT_WEBHOOK \
            --print-summary

Then update the Ruby script to read the URL from ENV['ALERT_WEBHOOK'] when --webhook-url-env is specified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/planarity_gephi_nightly.yml around lines 504 - 512, Do not
pass the secret via CLI; instead export ALERT_WEBHOOK as an environment variable
to the Ruby process and modify the script to accept a switch that reads from
ENV. Update the workflow invocation of
scripts/planarity_gephi_regression_notify.rb to remove the --webhook-url
"${ALERT_WEBHOOK}" argument and ensure ALERT_WEBHOOK is injected into the job
environment; then change the Ruby script to support a new flag (e.g.,
--webhook-url-env) and when that flag is present have the script read
ENV['ALERT_WEBHOOK'] rather than relying on a CLI --webhook-url value.
.github/workflows/planarity_gephi_nightly.yml-259-264 (1)

259-264: ⚠️ Potential issue | 🟡 Minor

Address Python 3.12+ tarfile.extractall() deprecation and harden archive extraction.

The tarfile.extractall() call emits a DeprecationWarning in Python 3.12+ when the filter parameter is omitted and will change to safer defaults in Python 3.14. Add filter='data' to strip dangerous path components.

For zipfile.extractall(), while the stdlib module has built-in protections against path traversal, the official Python documentation still recommends validating archive member paths for defense-in-depth. Consider adding a path validation check before extraction:

if asset_name.lower().endswith(".zip"):
    with zipfile.ZipFile(archive_path, "r") as zf:
        for info in zf.infolist():
            target = (ex_dir / info.filename).resolve()
            if not str(target).startswith(str(ex_dir.resolve())):
                raise SystemExit(f"Zip entry escapes target dir: {info.filename}")
        zf.extractall(ex_dir)
elif asset_name.lower().endswith(".tar.gz"):
    with tarfile.open(archive_path, "r:gz") as tf:
        tf.extractall(ex_dir, filter='data')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/planarity_gephi_nightly.yml around lines 259 - 264, The
archive extraction code using zipfile.ZipFile(...) and tarfile.open(..., "r:gz")
must be hardened and updated for Python 3.12+; before calling
zf.extractall(ex_dir) validate each zip member path by resolving (ex_dir /
info.filename).resolve() and ensure it starts with ex_dir.resolve(), raising an
error if it escapes the target directory, and for tarfile use
tf.extractall(ex_dir, filter='data') to avoid the DeprecationWarning and strip
dangerous path components; update the extraction branches that check
asset_name.lower().endswith(".zip") and asset_name.lower().endswith(".tar.gz")
to implement these changes around archive_path, zf, and tf.
Tests/cut_nodes_test.cc-1069-1086 (1)

1069-1086: ⚠️ Potential issue | 🟡 Minor

FreeFunctionConsistentWithClass: free-function result's arc identity is never verified.

The test confirms b_class contains pend (line 1085) but only checks that b_free.size() == b_class.size(). If b_free returns a different arc of the same count, the test passes silently.

🐛 Proposed fix
  EXPECT_EQ(b_class.size(), 1U);
  EXPECT_TRUE(bridge_set(b_class).exist(pend));
+ EXPECT_TRUE(bridge_set(b_free).exist(pend));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/cut_nodes_test.cc` around lines 1069 - 1086, The test
FreeFunctionConsistentWithClass currently only checks sizes, so add an identity
check that the free-function result actually contains the same arc as the class
result: after computing b_free with find_bridges(g, nodes[0]) assert that
bridge_set(b_free).exist(pend) (or compare bridge_set(b_free) ==
bridge_set(b_class)) so the exact arc pend is present; update the assertions in
the TEST(Bridges, FreeFunctionConsistentWithClass) to verify b_free contains
pend in addition to the size checks.
Dominators.H-510-522 (1)

510-522: ⚠️ Potential issue | 🟡 Minor

Dominance frontier computation may produce duplicate entries.

When multiple predecessors of a join point b share an ancestor on the walk up to idom(b), bnode is appended to that ancestor's frontier list multiple times. Since DynList is used rather than a set, the frontier can contain duplicates.

This doesn't break SSA phi-placement (placement is idempotent) but violates the set semantics documented for DF.

Proposed fix: use a set to track already-added nodes
          // Walk up from each predecessor to idom(b)
+          DynSetTree<long> already_in_df;
          for (auto it = preds.get_it(); it.has_curr(); it.next_ne())
            {
              long runner = it.get_curr();
              while (runner != idom_arr[b] and runner != -1)
                {
-                  df.find(vertex[runner]).append(bnode);
+                  if (not already_in_df.contains(runner))
+                    {
+                      already_in_df.insert(runner);
+                      df.find(vertex[runner]).append(bnode);
+                    }
                  runner = idom_arr[runner];
                }
            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dominators.H` around lines 510 - 522, The current dominance-frontier loop
appends bnode for each ancestor visited and can add duplicates when multiple
predecessors share ancestors; modify the loop in the computation that iterates
preds, idom_arr, vertex and df so it tracks which runner nodes have already had
bnode added for this b (e.g., create a local temporary set or bitmap "seen"
cleared per b) and only call df.find(vertex[runner]).append(bnode) when seen
does not contain runner, then mark runner as seen; this preserves DF as a set
and avoids duplicate entries.
Min_Cost_Matching.H-400-433 (1)

400-433: ⚠️ Potential issue | 🟡 Minor

Document functor operator() overloads with full Doxygen tags.

The public operator() methods in both functor wrappers lack @brief, @param, @return, and @throws tags. Please add full Doxygen blocks to satisfy the public API documentation rule. As per coding guidelines “All public member functions must have @brief, @param, @return, and @throws Doxygen tags.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Min_Cost_Matching.H` around lines 400 - 433, Add full Doxygen comment blocks
to both public operator() overloads: the one returning
Min_Cost_Matching_Result<long long> and
Compute_Minimum_Cost_Perfect_General_Matching::operator() so they include
`@brief`, `@param` g (describe graph input), `@param` matching (describe matching
output list), `@return` (describe the result type), and `@throws` (list possible
exceptions such as allocation or runtime errors). Place the block immediately
above each operator() declaration and ensure parameter names match exactly (g,
matching) and the `@return` text matches the concrete return types
(Min_Cost_Matching_Result<long long> and Min_Cost_Perfect_Matching_Result<long
long>).
Hungarian.H-344-352 (1)

344-352: ⚠️ Potential issue | 🟡 Minor

Reject initializer_list rows with zero columns.

rows = {{}} yields orig_cols_ == 0 but the ctor doesn’t guard it, while the DynMatrix ctor rejects empty matrices. Add a check for orig_cols_ == 0 to keep behavior consistent.

🔧 Proposed fix
     orig_rows_ = rows.size();
     orig_cols_ = rows.begin()->size();
+    ah_invalid_argument_if(orig_cols_ == 0)
+      << "Hungarian_Assignment: cost matrix must not be empty";
     n_ = orig_rows_ > orig_cols_ ? orig_rows_ : orig_cols_;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Hungarian.H` around lines 344 - 352, The constructor
Hungarian_Assignment(std::initializer_list<std::initializer_list<Cost_Type>>
rows) currently checks rows.size()==0 but not the case where rows contains
zero-length inner lists (rows = {{}}), which sets orig_cols_==0; add a guard
after computing orig_cols_ (or before setting n_) that uses the same error macro
(ah_invalid_argument_if(orig_cols_ == 0) << "Hungarian_Assignment: cost matrix
must have at least one column") so the ctor rejects zero-column input
consistently with DynMatrix and prevents n_ from being set from an invalid
orig_cols_; reference Hungarian_Assignment, orig_cols_, orig_rows_, and n_.
Tests/min_cost_matching_test.cc-98-205 (1)

98-205: ⚠️ Potential issue | 🟡 Minor

Prefer Aleph containers for helper data structures.

These utilities use std::vector and std::unordered_*; project rules require Aleph equivalents (e.g., DynList, Array, OLhashTable, DynSetTree) when available. Please migrate to Aleph containers for consistency across the test suite. As per coding guidelines “MUST use Aleph containers when an equivalent exists. Standard library containers are ONLY permitted when no Aleph equivalent is available.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/min_cost_matching_test.cc` around lines 98 - 205, Replace standard
containers with Aleph equivalents: in build_graph replace std::vector<typename
GT::Node *> nodes with Aleph::Array<typename GT::Node*> (or DynList if dynamic
push_back semantics are required) and use its insert/at APIs when creating
nodes; in verify_matching replace std::unordered_set<typename GT::Arc *>
arcs_in_graph and std::unordered_set<typename GT::Node *> used_nodes with
Aleph::DynSetTree<typename GT::Arc*> and Aleph::DynSetTree<typename GT::Node*>
respectively (use GT::Arc_Iterator and matching iterator the same way); in
normalize_simple_edges replace std::unordered_map<Pair,long long,Pair_Hash> best
with Aleph::OLhashTable<Pair,long long,Pair_Hash> and replace result
std::vector<Cost_Edge> with an Aleph::DynList<Cost_Edge> or
Aleph::Array<Cost_Edge> (choose DynList if you rely on push_back semantics),
preserving the same logic in build_graph, verify_matching, better and
normalize_simple_edges but using Aleph container APIs and constructors instead
of std::. Ensure to update any reserve/populate/iteration calls to the Aleph
equivalents and include the corresponding Aleph headers.
Tests/geom_algorithms_test_common.h-34-174 (1)

34-174: ⚠️ Potential issue | 🟡 Minor

Use Aleph error macros for degeneracy checks.

circumcenter_of() throws std::domain_error directly. The project requires Aleph error macros; please switch to ah_domain_error_if and include ah-errors.H.

🔧 Proposed fix
-#include <stdexcept>
+#include <ah-errors.H>
@@
-    if (den == 0)
-      throw std::domain_error("circumcenter_of: collinear triangle (zero area)");
+    ah_domain_error_if(den == 0)
+      << "circumcenter_of: collinear triangle (zero area)";

As per coding guidelines “ALWAYS use Aleph exception macros from ah-errors.H instead of raw throw statements.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/geom_algorithms_test_common.h` around lines 34 - 174, The
circumcenter_of function currently throws std::domain_error directly for the
collinear/zero-area check; change this to use the Aleph error macro by including
ah-errors.H and replacing the throw with ah_domain_error_if(den == 0,
"circumcenter_of: collinear triangle (zero area)"); locate the check in
circumcenter_of (function name) and remove the raw throw, and add the include
for ah-errors.H near the other includes at the top of the file.
Examples/min_mean_cycle_example.cc-69-122 (1)

69-122: ⚠️ Potential issue | 🟡 Minor

Persistent cout state mutation leaks out of print_result; use std::format.

cout << fixed << setprecision(6) (line 81) is set inside the function body after the early return, so it only fires when a cycle exists. Because cout formatting flags are global, any subsequent output — including the value-only block at line 161 — silently inherits fixed/setprecision(6). If the first call hits the early-return path (no cycle), the later minimum_mean print gets the default (un-formatted) representation.

Per coding guidelines, prefer std::format for type-safe string formatting over stream manipulators.

♻️ Proposed refactor using `std::format`
+#include <format>
 ...
 void print_result(const Graph & g,
                   const string & title,
                   const Min_Mean_Cycle_Result<Graph, double> & r)
 {
-  cout << title << '\n';
+  cout << std::format("{}\n", title);

   if (not r.has_cycle)
     {
-      cout << "  no directed cycle\n\n";
+      cout << "  no directed cycle\n\n"s;
       return;
     }

-  cout << fixed << setprecision(6);
-  cout << "  minimum mean = " << r.minimum_mean << '\n';
+  cout << std::format("  minimum mean = {:.6f}\n", r.minimum_mean);

   if (r.cycle_length == 0)
     {
-      cout << "  witness cycle unavailable\n\n";
+      cout << "  witness cycle unavailable\n\n"s;
       return;
     }

-  const long double witness_mean = static_cast<long double>(r.cycle_total_cost)
-                                   / static_cast<long double>(r.cycle_length);
-  cout << "  witness cycle: cost=" << r.cycle_total_cost
-       << ", length=" << r.cycle_length
-       << ", mean=" << witness_mean << '\n';
+  const double witness_mean = r.cycle_total_cost
+                              / static_cast<double>(r.cycle_length);
+  cout << std::format("  witness cycle: cost={:.6f}, length={}, mean={:.6f}\n",
+                      r.cycle_total_cost, r.cycle_length, witness_mean);
 ...

As per coding guidelines: "Use std::format for type-safe string formatting (prefer over printf/streams)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` around lines 69 - 122, The function
print_result mutates global cout formatting with "cout << fixed <<
setprecision(6)" which leaks outside the function; replace stream manipulators
with std::format-based, local formatting. Remove the cout << fixed <<
setprecision(6) line and format numeric outputs (r.minimum_mean and
witness_mean, and any numeric arc/node info) using std::format with a 6-decimal
format specifier (e.g. std::format("{:.6f}", value)), then pass those formatted
strings to cout (or build a single formatted string for each line); ensure you
`#include` <format> and keep all other logic in print_result (use
r.cycle_total_cost, r.cycle_length, r.minimum_mean, r.cycle_nodes, r.cycle_arcs,
g.get_src_node, g.get_tgt_node, arc->get_info(), node->get_info() to locate
places to change).
🧹 Nitpick comments (11)
Tests/ah-functional.cc (2)

1499-1501: Prefer std::abs over unqualified fabs for C++20 style.

fabs is the C <math.h> function. std::abs from <cmath> has a float overload in C++ and is the idiomatic choice.

♻️ Proposed fix
-    return fabs(a - b) <= tolerance;
+    return std::abs(a - b) <= tolerance;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ah-functional.cc` around lines 1499 - 1501, In the comparator
operator()(float a, float b) const replace the C fabs call with the C++
idiomatic std::abs (i.e., use std::abs(a - b) <= tolerance) and ensure <cmath>
is included so the float overload is available; update the body of operator()
accordingly and qualify the call with std:: to avoid relying on unqualified
lookup.

1546-1554: custom_predicate_with_rvalue duplicates existing tests without distinct coverage.

Both assertions mirror case_insensitive_strings_false and case_insensitive_strings_true exactly. If the intent is to test passing the container as an rvalue (e.g., all_unique(build_dynlist<string>(...), ...)), the test should be updated accordingly; otherwise it can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ah-functional.cc` around lines 1546 - 1554, The test
all_unique_overload::custom_predicate_with_rvalue currently duplicates existing
cases; update it to actually exercise the rvalue overload by calling all_unique
with a temporary container (e.g.,
all_unique(build_dynlist<string>("Apple","apple","Banana"),
CaseInsensitiveStringEqual()) and similarly for the true case), ensuring you use
the build_dynlist call directly rather than a named variable, or delete the test
if you prefer not to cover the rvalue overload; reference the test name
all_unique_overload, the all_unique function, build_dynlist, and
CaseInsensitiveStringEqual when making the change.
.github/workflows/planarity_gephi_nightly.yml (4)

37-40: Unused Ruby setup in prepare-tag-matrix job.

This job only runs an inline Python script. The Ruby setup adds unnecessary CI time (~15-30s) with no benefit. Consider removing it.

♻️ Proposed fix
-      - name: Setup Ruby
-        uses: ruby/setup-ruby@v1
-        with:
-          ruby-version: '3.3'
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/planarity_gephi_nightly.yml around lines 37 - 40, The
"Setup Ruby" step (uses: ruby/setup-ruby@v1, with ruby-version: '3.3') inside
the prepare-tag-matrix job is unused because the job only runs an inline Python
script; remove that step from the prepare-tag-matrix job so the workflow no
longer installs Ruby unnecessarily and reduces CI time.

86-94: GitHub API requests are unauthenticated — subject to 60 req/hour rate limit.

Both the tag-resolution step (here) and the per-matrix download step (line 188) call the GitHub API without an Authorization header. Unauthenticated requests share a 60 req/hr budget per IP. For a weekly schedule this is likely fine, but if runners share IPs or other workflows run concurrently, it could fail. Consider passing GITHUB_TOKEN for the 5,000 req/hr authenticated limit.

♻️ Suggested: add token header
              req = urllib.request.Request(
                  "https://api.github.com/repos/gephi/gephi/releases?per_page=100",
                  headers={
                      "Accept": "application/vnd.github+json",
                      "User-Agent": "aleph-planarity-gephi-nightly",
+                     "Authorization": f"Bearer {os.environ.get('GITHUB_TOKEN', '')}",
                  },
              )

And add GITHUB_TOKEN to the step's env:

        env:
          MANUAL_GEPHI_TAGS: ${{ github.event.inputs.gephi_tags }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Same pattern applies to the download step at line 188.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/planarity_gephi_nightly.yml around lines 86 - 94, The
GitHub API requests created with urllib.request.Request (variable req) are
unauthenticated and risk hitting the 60 req/hr limit; update the request headers
to include an Authorization header using the GITHUB_TOKEN (e.g.,
"Authorization": f"token {GITHUB_TOKEN}") and ensure the workflow step exposes
GITHUB_TOKEN in the step env (add GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
alongside MANUAL_GEPHI_TAGS). Apply the same header change to the other GitHub
API request used in the per-matrix download step (the call referenced around
line 188) so both requests use the authenticated 5,000 req/hr quota.

406-420: Mutating proc.returncode is fragile — use a dedicated exit-code variable.

Line 410 reassigns proc.returncode = 2 after the process has completed, which is unusual and could confuse future readers. A separate variable makes the intent clearer and avoids coupling to the CompletedProcess object.

♻️ Proposed refactor
+         exit_code = proc.returncode
          if proc.returncode == 0:
              payload = json.loads(proc.stdout)
              summary["overall_valid"] = bool(payload.get("overall_valid", False))
              if not summary["overall_valid"]:
-                 proc.returncode = 2
+                 exit_code = 2

          (out_dir / "nightly_case_summary.json").write_text(
              json.dumps(summary, indent=2, sort_keys=True) + "\n",
              encoding="utf-8",
          )

-         if proc.returncode != 0:
+         if exit_code != 0:
              print(proc.stdout)
              print(proc.stderr, file=sys.stderr)
-             raise SystemExit(proc.returncode)
+             raise SystemExit(exit_code)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/planarity_gephi_nightly.yml around lines 406 - 420, The
code mutates proc.returncode after the process completed; instead introduce a
dedicated exit_code variable (e.g., exit_code = proc.returncode) right after
obtaining proc, use payload to set summary["overall_valid"], and if not
summary["overall_valid"] set exit_code = 2 (not proc.returncode); continue to
write nightly_case_summary.json from summary and then check if exit_code != 0 to
print proc.stdout/proc.stderr and raise SystemExit(exit_code). This keeps proc
immutable and makes intent explicit.

539-543: Minor script-injection risk from direct ${{ }} interpolation in bash.

Line 542 interpolates ${{ steps.compare.outputs.num_regressions }} directly into a bash script. If the output contained shell metacharacters, they would be interpreted. While the value originates from a controlled Ruby script (likely just a number), the safer pattern is to pass it through an environment variable.

🛡️ Proposed fix
      - name: Enforce regression gate
        if: ${{ steps.compare.outputs.has_regressions == 'true' }}
        shell: bash
+       env:
+         NUM_REGRESSIONS: ${{ steps.compare.outputs.num_regressions }}
        run: |
          set -euo pipefail
          echo "Gephi nightly comparison detected regressions across tags." >&2
-         echo "num_regressions=${{ steps.compare.outputs.num_regressions }}" >&2
+         echo "num_regressions=${NUM_REGRESSIONS}" >&2
          exit 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/planarity_gephi_nightly.yml around lines 539 - 543, The
run block currently interpolates steps.compare.outputs.num_regressions directly
into the shell which can allow shell metacharacters to be interpreted; instead
export the output as an environment variable and reference the variable inside
the run script (e.g., add an env mapping for NUM_REGRESSIONS: ${{
steps.compare.outputs.num_regressions }} and then echo "$NUM_REGRESSIONS" inside
the run block), ensuring you quote the variable and keep set -euo pipefail in
place; update references to steps.compare.outputs.num_regressions in this job to
use the new NUM_REGRESSIONS env var.
K_Shortest_Paths.H (2)

192-209: Thread-local static state in Reverse_Arc_Distance is not re-entrant.

map_ptr and base_ptr are thread_local statics, making them safe across threads but not re-entrant within the same thread. If build_suffix_index_digraph were ever called from within a callback during another k-shortest-paths computation on the same thread, the pointers would be overwritten. The RAII guard Reset_Reverse_Distance (line 547) helps but doesn't prevent nested use.

This is an existing pattern in the codebase (cookie-based approaches have similar limitations), so it's acceptable as-is. A brief @warning noting non-re-entrant usage would be valuable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@K_Shortest_Paths.H` around lines 192 - 209, Reverse_Arc_Distance currently
uses thread_local static pointers (map_ptr, base_ptr) which are not re-entrant
within the same thread; add a brief documentation warning to the
Reverse_Arc_Distance declaration (and/or the Reset_Reverse_Distance RAII guard)
stating that build_suffix_index_digraph and any use of Reverse_Arc_Distance is
not re-entrant on the same thread and must not be invoked nested (mention
map_ptr and base_ptr by name and reference Reset_Reverse_Distance for the
existing guard) so callers are aware of the limitation.

238-267: get_node_at / get_arc_at are O(n) per call, used in inner loops.

These functions perform linear traversal on DynList to reach an index. They're called inside compose_candidate, compose_general_candidate, and the Yen spur iteration loop, making per-spur-iteration cost O(path_length²) rather than O(path_length).

For typical usage (modest path lengths and k), this is unlikely to be a bottleneck. If performance matters, consider caching a random-access snapshot (e.g., Array) of the path's nodes/arcs at the start of each iteration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@K_Shortest_Paths.H` around lines 238 - 267, get_node_at and get_arc_at
perform O(n) linear scans and are called repeatedly in inner loops (see
compose_candidate, compose_general_candidate, and the Yen spur iteration),
causing quadratic cost per spur; fix by creating a random-access snapshot (e.g.,
an Array or std::vector) of the path's nodes and arcs once at the start of each
compose_candidate/compose_general_candidate call and each Yen spur iteration,
then replace calls to get_node_at/get_arc_at with direct indexed access into
that snapshot (or iterate with a single indexed loop) to make per-path traversal
linear.
Dominators.H (1)

169-185: Recursive DFS may overflow the stack on deep graphs.

dfs() uses unbounded recursion proportional to the longest DFS path. For large graphs (or pathological ones like long chains), this can cause a stack overflow. The same concern applies to compress() on line 195, though path compression makes deep chains less likely there.

Consider converting to an iterative DFS using an explicit stack if you expect this to be used on very large graphs (100k+ nodes). For a compiler SSA pass on typical function-level CFGs this is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dominators.H` around lines 169 - 185, The recursive dfs function (dfs) can
overflow for deep graphs; replace it with an iterative DFS using an explicit
stack to perform the same pre-order numbering and initialization of
NODE_COUNTER, num_reachable, vertex, par, semi, label_arr and anc, and traverse
edges via Out_Iterator/ get_tgt_node while pushing child frames instead of
recursing; ensure the iterative logic sets par[v_num] to the parent index and
only visits nodes with NODE_COUNTER == -1. Also make compress iterative
(compress) to avoid recursion in deepest ancestor chains, applying iterative
path-compression on anc/label_arr as in the current recursive logic.
.github/workflows/ci.yml (1)

236-241: Hardcoded /tmp output paths are fragile for CI.

The example writes to /tmp/planarity_k33_certificate.graphml and .gexf, and the CI step checks those exact paths. If the example changes its output directory, this step will fail without a clear error message pointing to the root cause.

Consider passing an output directory argument to the example or setting an environment variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 236 - 241, The CI step "Generate
certificate exchange artifacts" hardcodes /tmp output files; update the
invocation of ./build-planarity-visual/Examples/planarity_test_example to accept
an explicit output directory (or use an environment variable like
PLANARITY_OUT_DIR) and write artifacts there instead of /tmp, then change the
test checks to look for $PLANARITY_OUT_DIR/planarity_k33_certificate.graphml and
$PLANARITY_OUT_DIR/planarity_k33_certificate.gexf (or the chosen output path
variable); ensure the workflow exports the variable before running the example
and that the example binary (or its wrapper) reads the arg/ENV and writes to
that directory so CI is no longer tied to /tmp.
Examples/min_mean_cycle_example.cc (1)

90-95: Add a comment clarifying the relationship between witness_mean and r.minimum_mean.

witness_mean recomputes the mean from the witness cycle's raw cost and length, while r.minimum_mean is the value returned by the algorithm. A reader cannot tell whether these are expected to be equal or are showing two distinct quantities. A one-line comment would make the intent clear (e.g., "sanity-check: witness cycle mean should equal the reported minimum mean").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` around lines 90 - 95, Add a one-line
comment above the computation/printing of witness_mean clarifying that
witness_mean is recomputed from the witness cycle's raw cost and length and is
intended as a sanity-check against r.minimum_mean (i.e., the algorithm's
reported minimum mean), so readers know whether they should expect these to
match or could differ.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/planarity_gephi_nightly.yml:
- Around line 230-234: The "win" substring check in the Windows branch (where
runner_os == "Windows") falsely matches "darwin"; update the condition that
currently uses '"win" in lower' to a precise check such as checking for "win-"
or "win64" tokens or using lower.startswith("win") or a word-boundary regex
(e.g., r'\bwin\b') against the variable lower before adding to score, so only
true Windows asset names increment score (refer to runner_os, lower, and score
in the diff).

In `@Examples/k_shortest_paths_example.cc`:
- Around line 59-79: Replace the STL containers in Scenario and build_graph with
Aleph equivalents: change Scenario::edges from
std::vector<std::tuple<size_t,size_t,long long>> to an Aleph container (e.g.,
aleph::DynArray of aleph::Array or a small Edge struct) and change the nodes
buffer in build_graph from std::vector<Graph::Node*> to an Aleph
DynArray/DynList; update build_graph to reserve/append using the Aleph API and
iterate accessing tuple elements via array indices or the Edge struct instead of
std::tuple structured bindings. Ensure all uses of Scenario::edges and nodes
(inserting arcs and nodes) are updated to the Aleph types and methods
(insert/push_back semantics) so no std::vector/std::tuple remain.

In `@Examples/weighted_blossom_example.cc`:
- Around line 83-113: Replace all std::vector/std::set usage in Scenario,
Solve_Output, and build_graph with the corresponding Aleph containers: use
aleph::DynArray for fixed-indexable sequences (nodes and matched_pairs),
aleph::DynArray or aleph::DynList for edges depending on whether random access
is required, and aleph::DynList/DynSetTree where a list or set semantics are
intended; specifically update struct Scenario (nodes, edges), struct
Solve_Output (matched_pairs), and the local nodes container inside template
build_graph<GT> to use aleph containers (e.g., aleph::DynArray<typename
GT::Node*> for the nodes buffer and aleph::DynArray/aleph::DynList for edges),
and adjust any iterator/index usage accordingly so the code compiles with the
Aleph container APIs.

In `@Hungarian.H`:
- Around line 492-508: The template hungarian_max_assignment currently defines
Promoted as std::make_signed_t<std::common_type_t<Cost_Type, long long>>, which
is ill-formed for floating-point Cost_Type; change Promoted to use
std::conditional_t<std::is_floating_point_v<std::common_type_t<Cost_Type, long
long>>, std::common_type_t<Cost_Type, long long>,
std::make_signed_t<std::common_type_t<Cost_Type, long long>>> so floating-point
common types are used as-is while integral types are converted to a signed
promoted type; keep the rest of the function (negated allocation and negation
via static_cast<Promoted>) unchanged.

In `@Tests/blossom_test.cc`:
- Around line 48-123: Replace standard containers in build_graph and
verify_matching with Aleph containers: change the local nodes std::vector to
Aleph::DynArray<typename GT::Node*> in build_graph and adjust its reserve/insert
usage accordingly; change the edges parameter from std::vector to
Aleph::DynArray<std::pair<size_t,size_t>> (or accept both by adding an overload)
so callers supply Aleph arrays. In verify_matching replace arcs_in_graph
std::unordered_set<typename GT::Arc*> with Aleph::OLhashTable<typename GT::Arc*>
(or Aleph::DynSetTree if ordered set semantics are required) and replace
used_nodes std::unordered_set<typename GT::Node*> with
Aleph::DynSetTree<typename GT::Node*>; update iteration/insert/lookup calls to
use the Aleph container APIs while keeping logic in functions build_graph and
verify_matching unchanged.
- Around line 491-520: Add an opt-in deterministic performance regression test
for blossom by creating a new typed test (similar to
DenseRandomStressCrossBackendAgreement) that uses a fixed RNG seed, larger n
(e.g. 500–2000), and constructs the graph via build_graph and runs run_blossom
while measuring elapsed time or operation count; guard it behind an environment
flag or gtest flag (so it doesn't run by default) and assert the
runtime/operation is below a chosen threshold while still verifying correctness
with verify_matching; name the test to reflect perf intent (e.g.,
BlossomMatchingPerfRegression) and keep the measurement deterministic by fixing
the seed and limiting nondeterminism.

In `@Tests/dominators_test.cc`:
- Around line 516-561: Add a deterministic performance regression test for
dominators: create a new opt-in test (e.g., TEST(Dominators,
PerformanceLargeRandomDAG)) or guard an extended run in the existing
LargeRandomDAG with an env var or macro (e.g., PERFORMANCE_TESTS) so it only
runs when enabled; build a larger deterministic DAG (fixed seed RNG, e.g., N ~
5000) and exercise compute_dominators and build_dominator_tree using the same
APIs (compute_dominators, idom_map, build_dominator_tree), measure elapsed
wall-clock time with std::chrono (or count operations if an instrumentation hook
exists), and assert the runtime or operation count is below a chosen budget via
EXPECT_LT/EXPECT_LE to catch regressions; ensure test is deterministic and
documents the opt-in mechanism.
- Around line 38-73: Replace standard containers in make_nodes and idom_map with
Aleph equivalents: change make_nodes to return a DynArray<DG::Node*> (or
DynList<DG::Node*> if preferred) instead of std::vector and use
DynArray::push_back (and reserve if available) to populate it; update its
signature and callers accordingly. Change idom_map to return a
DynMapTree<int,int> instead of std::map<int,int>, and replace
m[node->get_info()] = ... with the appropriate DynMapTree insertion (e.g.,
m.insert(key, value) or m.put(key, value) depending on the API) while keeping
the same iteration over idoms.get_it() and the same logic for mapping
idom->get_info() or -1. Ensure you include the correct Aleph headers/types
(DynArray/DynMapTree/DynList) and update any code that consumes these helpers to
use the Aleph container API.

In `@Tests/k_shortest_paths_test.cc`:
- Around line 337-379: Add an opt-in deterministic performance regression test
for k-shortest paths: create a new TEST (e.g.,
KShortestPathsTest.PerfRegression) that uses fixed RNG seeds and larger graph
sizes than RandomSmallGraphsMatchExactOracle, runs yen_k_shortest_paths and
eppstein_k_shortest_paths (and optionally exact_k_shortest_simple_paths for
smaller baseline) and asserts they finish within a predefined budget (wall-clock
time or operation count) using a guard like PERF_TESTS_ENABLED or an environment
variable to opt-in; ensure the test documents the seed, K, graph generation
routine (random_graph_edges/build_graph) and the exact budget so future changes
can be detected deterministically.
- Around line 61-137: The tests use std::vector and std::unordered_set in
helpers; replace them with Aleph containers per guidelines: use aleph::DynArray
(or aleph::DynList if list semantics are needed) for vectors in
Canonical_Path.nodes, Built_Graph.nodes, extract_node_ids return type and the
local ids/ out arrays in normalize_results/build_graph, and replace
std::unordered_set<Node*> in is_simple_path with the Aleph OLhashTable or
DynSetTree variant for pointer membership; update function signatures and local
iterators that reference Canonical_Path, Built_Graph, build_graph,
extract_node_ids, is_simple_path, and normalize_results accordingly so all
uses/initializations and comparisons use the Aleph container APIs instead of
std::vector/unordered_set while preserving the same equality/ordering logic in
operator== and the sort comparator.

In `@Tests/lca_test.cc`:
- Around line 482-547: Add a deterministic performance regression test that
exercises large trees and asserts it completes within a budgeted time or
operation count; create a new TYPED_TEST (e.g., LcaTypedTest.PerfRegression or a
separate test fixture LcaPerfTest) that uses a fixed rng seed, builds a large
tree (e.g., n = 100000 with make_random_tree_edges and
build_graph_with_unit_arcs), constructs Binary_Lifting_LCA<Graph> and
Euler_RMQ_LCA<Graph>, then runs a large number of LCA and distance queries
(reusing node_pick from RandomTreesAgainstNaiveOracle) while measuring elapsed
time with std::chrono and ASSERT_LT(elapsed_ms, kBudgetMs) or a
counted-operations assert; guard the test behind a compile-time or runtime
opt-in (e.g., preprocessor macro LCA_PERF_TEST or a GoogleTest filter) so it is
disabled by default, and reference the existing symbols Binary_Lifting_LCA,
Euler_RMQ_LCA, Naive_Tree_Oracle, make_random_tree_edges, and
build_graph_with_unit_arcs to locate where to add it.
- Around line 66-146: Replace all std::vector usages in Naive_Tree_Oracle with
Aleph containers: change the constructor parameter from const
std::vector<std::pair<size_t,size_t>> & edges to the Aleph equivalent (e.g.
DynArray<std::pair<size_t,size_t>> or DynList) and update member types parent_,
depth_, tin_, tout_ to DynArray<size_t> and adj_ to DynArray<DynList<size_t>>
(or DynArray<Array<size_t>> as preferred). Update all call sites inside
Naive_Tree_Oracle: use the Aleph container constructor/init (size n_), replace
edges.size() with the Aleph size/length accessor, replace adj_[u].push_back(v)
with the Aleph append/insert method, and adjust iteration over edges and adj_ to
use the Aleph iteration API. Also add the necessary Aleph headers/using
directives and ensure the dfs, ah_runtime_error_if checks and other code refer
to the new types.

In `@Tests/min_cost_matching_test.cc`:
- Around line 741-839: Add a deterministic performance-regression test that
exercises the min-cost-matching backends on a single large, fixed-seed instance
and is disabled by default (e.g., name the test with the DISABLED_ prefix or
guard with a PERF_TESTS macro); construct a large graph (e.g., n ~ 1000–2000 or
other target size) using a fixed std::mt19937_64 seed, reuse the existing edge
construction pattern (calls to Cost_Edge emplace_back as in the tests above),
and call the same entry points used in correctness tests
(exact_minimum_cost_matching_optimum /
exact_minimum_cost_perfect_matching_optimum and solve_backend /
solve_backend_perfect for the List_Backend, SGraph_Backend, AGraph_Backend);
measure elapsed time with std::chrono around the backend calls and assert it
meets a reasonable threshold (or log the duration) so CI can detect regressions
while keeping the test disabled by default.

In `@Tests/planarity_test.cc`:
- Around line 440-479: The tests call run_external_certificate_validator(...)
unconditionally and write to hard-coded /tmp paths; update
run_external_certificate_validator and the tests to (1) use
std::filesystem::temp_directory_path() to build stdout/stderr paths instead of
"/tmp" for portability, and (2) add pre-check guards before asserting rc == 0:
verify find_repo_root() and script existence (already checked in
run_external_certificate_validator) and the specific tool availability flags
used elsewhere (e.g., has_nx, has_gephi_usable or equivalent checks for
Ruby/NetworkX/Gephi) and only assert success when those prerequisites are
present—mirror the conditional patterns used in
ExternalCertificateValidatorNetworkxModeIsPortable and
ExternalCertificateValidatorGephiModeIsPortable and apply them to tests such as
ExternalCertificateValidatorCanListGephiTemplates,
ExternalCertificateValidatorCanListGephiRenderProfiles and the render tests that
call run_external_certificate_validator.

---

Outside diff comments:
In `@CMakeLists.txt`:
- Around line 82-126: HLIST is missing the new public headers Dominators.H and
Hungarian.H so they aren't installed; add "Dominators.H" and "Hungarian.H" to
the HLIST variable (the list used by install(FILES ${HLIST} ...)) so those
headers are packaged/installed for downstream consumers, ensuring you insert the
names alongside the other .H entries in the HLIST definition.

---

Duplicate comments:
In `@Tests/cut_nodes_test.cc`:
- Around line 938-948: The previous concern about incorrect Aleph-container
usage has been resolved; confirm no change is needed: ensure the helper
bridge_set and the arc collection variable (e.g., DynList<Graph::Arc *> arcs)
use DynSetTree<Graph::Arc *> and DynList<Graph::Arc *>::Iterator respectively,
and leave the bridge_set function and its usage as shown (no modifications
required).

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 236-241: The CI step "Generate certificate exchange artifacts"
hardcodes /tmp output files; update the invocation of
./build-planarity-visual/Examples/planarity_test_example to accept an explicit
output directory (or use an environment variable like PLANARITY_OUT_DIR) and
write artifacts there instead of /tmp, then change the test checks to look for
$PLANARITY_OUT_DIR/planarity_k33_certificate.graphml and
$PLANARITY_OUT_DIR/planarity_k33_certificate.gexf (or the chosen output path
variable); ensure the workflow exports the variable before running the example
and that the example binary (or its wrapper) reads the arg/ENV and writes to
that directory so CI is no longer tied to /tmp.

In @.github/workflows/planarity_gephi_nightly.yml:
- Around line 37-40: The "Setup Ruby" step (uses: ruby/setup-ruby@v1, with
ruby-version: '3.3') inside the prepare-tag-matrix job is unused because the job
only runs an inline Python script; remove that step from the prepare-tag-matrix
job so the workflow no longer installs Ruby unnecessarily and reduces CI time.
- Around line 86-94: The GitHub API requests created with urllib.request.Request
(variable req) are unauthenticated and risk hitting the 60 req/hr limit; update
the request headers to include an Authorization header using the GITHUB_TOKEN
(e.g., "Authorization": f"token {GITHUB_TOKEN}") and ensure the workflow step
exposes GITHUB_TOKEN in the step env (add GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN
}} alongside MANUAL_GEPHI_TAGS). Apply the same header change to the other
GitHub API request used in the per-matrix download step (the call referenced
around line 188) so both requests use the authenticated 5,000 req/hr quota.
- Around line 406-420: The code mutates proc.returncode after the process
completed; instead introduce a dedicated exit_code variable (e.g., exit_code =
proc.returncode) right after obtaining proc, use payload to set
summary["overall_valid"], and if not summary["overall_valid"] set exit_code = 2
(not proc.returncode); continue to write nightly_case_summary.json from summary
and then check if exit_code != 0 to print proc.stdout/proc.stderr and raise
SystemExit(exit_code). This keeps proc immutable and makes intent explicit.
- Around line 539-543: The run block currently interpolates
steps.compare.outputs.num_regressions directly into the shell which can allow
shell metacharacters to be interpreted; instead export the output as an
environment variable and reference the variable inside the run script (e.g., add
an env mapping for NUM_REGRESSIONS: ${{ steps.compare.outputs.num_regressions }}
and then echo "$NUM_REGRESSIONS" inside the run block), ensuring you quote the
variable and keep set -euo pipefail in place; update references to
steps.compare.outputs.num_regressions in this job to use the new NUM_REGRESSIONS
env var.

In `@Dominators.H`:
- Around line 169-185: The recursive dfs function (dfs) can overflow for deep
graphs; replace it with an iterative DFS using an explicit stack to perform the
same pre-order numbering and initialization of NODE_COUNTER, num_reachable,
vertex, par, semi, label_arr and anc, and traverse edges via Out_Iterator/
get_tgt_node while pushing child frames instead of recursing; ensure the
iterative logic sets par[v_num] to the parent index and only visits nodes with
NODE_COUNTER == -1. Also make compress iterative (compress) to avoid recursion
in deepest ancestor chains, applying iterative path-compression on anc/label_arr
as in the current recursive logic.

In `@Examples/min_mean_cycle_example.cc`:
- Around line 90-95: Add a one-line comment above the computation/printing of
witness_mean clarifying that witness_mean is recomputed from the witness cycle's
raw cost and length and is intended as a sanity-check against r.minimum_mean
(i.e., the algorithm's reported minimum mean), so readers know whether they
should expect these to match or could differ.

In `@K_Shortest_Paths.H`:
- Around line 192-209: Reverse_Arc_Distance currently uses thread_local static
pointers (map_ptr, base_ptr) which are not re-entrant within the same thread;
add a brief documentation warning to the Reverse_Arc_Distance declaration
(and/or the Reset_Reverse_Distance RAII guard) stating that
build_suffix_index_digraph and any use of Reverse_Arc_Distance is not re-entrant
on the same thread and must not be invoked nested (mention map_ptr and base_ptr
by name and reference Reset_Reverse_Distance for the existing guard) so callers
are aware of the limitation.
- Around line 238-267: get_node_at and get_arc_at perform O(n) linear scans and
are called repeatedly in inner loops (see compose_candidate,
compose_general_candidate, and the Yen spur iteration), causing quadratic cost
per spur; fix by creating a random-access snapshot (e.g., an Array or
std::vector) of the path's nodes and arcs once at the start of each
compose_candidate/compose_general_candidate call and each Yen spur iteration,
then replace calls to get_node_at/get_arc_at with direct indexed access into
that snapshot (or iterate with a single indexed loop) to make per-path traversal
linear.

In `@Tests/ah-functional.cc`:
- Around line 1499-1501: In the comparator operator()(float a, float b) const
replace the C fabs call with the C++ idiomatic std::abs (i.e., use std::abs(a -
b) <= tolerance) and ensure <cmath> is included so the float overload is
available; update the body of operator() accordingly and qualify the call with
std:: to avoid relying on unqualified lookup.
- Around line 1546-1554: The test
all_unique_overload::custom_predicate_with_rvalue currently duplicates existing
cases; update it to actually exercise the rvalue overload by calling all_unique
with a temporary container (e.g.,
all_unique(build_dynlist<string>("Apple","apple","Banana"),
CaseInsensitiveStringEqual()) and similarly for the true case), ensuring you use
the build_dynlist call directly rather than a named variable, or delete the test
if you prefer not to cover the rvalue overload; reference the test name
all_unique_overload, the all_unique function, build_dynlist, and
CaseInsensitiveStringEqual when making the change.

Copilot AI review requested due to automatic review settings February 22, 2026 17:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 59 changed files in this pull request and generated 10 comments.

@lrleon
Copy link
Copy Markdown
Owner Author

lrleon commented Feb 22, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2ab377a81

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (7)
Blossom.H (1)

195-222: LCA fallback return b at line 221 is technically unreachable but adds safety.

When find_augmenting_path calls lca(v, u), both nodes are in the same alternating tree. The first walk from a to the root marks all bases; the second walk from b must hit one of them before reaching the root. The return b at line 221 acts as a safety net — if it were ever reached, it would indicate a bug in the tree invariants rather than produce a correct result. Consider adding a debug assertion here.

💡 Optional: add a debug assertion
           b = static_cast<size_t>(parent[static_cast<size_t>(match[b])]);
          }

+       // Should be unreachable: both a and b are in the same alternating tree
+       assert(false && "lca: b-walk reached root without finding visited base");
        return b;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Blossom.H` around lines 195 - 222, The final "return b" in lca(size_t a,
size_t b) is only a safety fallback and should be asserted as unreachable; add a
debug assertion (e.g., assert(false && "lca fallback reached") or a
platform-specific unreachable check) just before the return to document the
invariant and help catch logic bugs at runtime, referencing the lca function and
leaving the fallback return in place for safety in release builds.
Tests/dominators_test.cc (2)

705-718: Post-dominator ipdom_map helper uses std::map instead of DynMapTree.

The dominator-side idom_map (line 66) was updated to use DynMapTree<int, int>, but the post-dominator counterpart still uses std::map<int, int>. This inconsistency violates the Aleph container guideline. As per coding guidelines: "MUST use Aleph containers when an equivalent exists."

♻️ Proposed fix
-  std::map<int, int> ipdom_map(const DynList<std::pair<DG::Node*,
-                                                        DG::Node*>> & ipdoms)
+  DynMapTree<int, int> ipdom_map_post(const DynList<std::pair<DG::Node*,
+                                                               DG::Node*>> & ipdoms)
   {
-    std::map<int, int> m;
+    DynMapTree<int, int> m;
     for (auto it = ipdoms.get_it(); it.has_curr(); it.next_ne())
       {
         auto [node, ipdom] = it.get_curr();
-        m[node->get_info()] = ipdom ? ipdom->get_info() : -1;
+        m.insert(node->get_info(), ipdom ? ipdom->get_info() : -1);
       }
     return m;
   }

Note: All call sites that use ipdom_map(...) in the PostDominators tests would need to be updated accordingly (using DynMapTree API for lookups).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/dominators_test.cc` around lines 705 - 718, The ipdom_map helper
currently returns a std::map<int,int>; replace it with Aleph's
DynMapTree<int,int> so it matches the idom_map pattern: change the function
return type and local variable m to DynMapTree<int,int>, insert entries via
m.set(key, value) (or the DynMapTree equivalent) while iterating the DynList of
std::pair<DG::Node*,DG::Node*> (ipdoms) and return m; update all PostDominators
test call sites to use DynMapTree lookup APIs instead of std::map::operator[] or
find to accommodate the container change.

686-698: StringTypedGraph test uses std::map<std::string, std::string> instead of Aleph container.

For consistency with the rest of the test file, consider using DynMapTree<std::string, std::string>. As per coding guidelines: "Use DynMapTree<K, V> instead of std::map<K, V>."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/dominators_test.cc` around lines 686 - 698, Replace the std::map usage
in the dominators test with the project's DynMapTree<K,V> container: change the
variable declaration m from std::map<std::string,std::string> to
DynMapTree<std::string,std::string>, and when populating it in the loop that
iterates idoms via idoms.get_it() use DynMapTree's insert/assignment API to set
m[node->get_info()] = idom ? idom->get_info() : "none"; leave the EXPECT_EQ
checks for m["entry"], m["body"], and m["exit"] unchanged so the assertions use
the DynMapTree instead of std::map.
Tests/min_cost_matching_test.cc (1)

56-60: Pervasive use of std::vector, std::unordered_set, and std::unordered_map in test utilities.

The test helpers (build_graph, verify_matching, normalize_simple_edges, the DP oracle, and all test cases) use standard containers extensively. While some uses are justified (e.g., the bitmask-indexed memo table in the DP oracle needs O(1) random access by index), many std::vector<Cost_Edge> instances and std::unordered_set could be replaced with Array<Cost_Edge> / DynArray<Cost_Edge> and Aleph hash containers. As per coding guidelines: "MUST use Aleph containers when an equivalent exists."

Given the scope of changes needed and that this is test infrastructure, consider this for a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/min_cost_matching_test.cc` around lines 56 - 60, Many test utilities
(build_graph, verify_matching, normalize_simple_edges, the DP oracle, and test
cases) use std::vector, std::unordered_set, and std::unordered_map; replace
these with the project's Aleph container equivalents (Array/ DynArray for
vectors and Aleph hash containers for unordered_* where equivalents exist) to
comply with the coding guideline. Update types and construction/iteration code
in functions build_graph, verify_matching, normalize_simple_edges, the DP oracle
(memo usage that needs O(1) random access should keep an Array or DynArray as
appropriate), and all test cases to use Array<Cost_Edge>/DynArray<Cost_Edge>
instead of std::vector<Cost_Edge>, and replace
std::unordered_set/std::unordered_map usages with the Aleph hash set/map types;
keep std::vector only where no Aleph equivalent exists and ensure API callsites
(emplace/insert, size, iteration, indexing) are adjusted to the Aleph container
methods.
Examples/planarity_test_example.cc (1)

274-289: File writes lack error checking.

The ofstream objects for writing certificate files (JSON, DOT, GraphML, GEXF) are opened and written to without checking for errors. For an example program this is tolerable, but in a production context you'd want to verify that the files were written successfully.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/planarity_test_example.cc` around lines 274 - 289, The current write
blocks that create ofstream objects (fj, fd, fg, fx) and stream the contents
(json, dot, graphml, gexf) do not check whether the files opened or whether the
writes succeeded; update each block that opens json_path, dot_path, graphml_path
and gexf_path to verify the stream opened (e.g., test fj/is_open or operator
bool) and check for write errors after writing (e.g., check fail() or good()),
and on error return/throw or log a clear message including the path and the
stream error state so failures are detected and propagated instead of silently
ignored.
Tests/lca_test.cc (1)

117-131: Recursive DFS in Naive_Tree_Oracle is safe for the current test sizes but could overflow on degenerate linear trees at scale.

The dfs method is recursive with depth proportional to tree height. For the random trees used in tests (expected O(log n) height), this is fine. However, if the oracle were ever used with a linear chain of 100k nodes, it would blow the stack. Since it's only a test utility and the tree generator produces balanced random trees, this is acceptable as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/lca_test.cc` around lines 117 - 131, The recursive dfs in
Naive_Tree_Oracle (function dfs) can overflow for deep/linear trees; replace it
with an iterative DFS using an explicit stack to simulate recursion so parent_,
depth_, tin_, tout_, and timer_ are set correctly: push a tuple/state for the
root (node, parent, depth, entering flag), on entering set parent_[u],
depth_[u], tin_[u]=timer_++, iterate adjacency via adj_[u] to push children
(skip parent), and on exiting set tout_[u]=timer_-1; ensure you reference and
update timer_, adj_, parent_, depth_, tin_, and tout_ consistently to preserve
current behavior without using the call stack.
Tests/blossom_test.cc (1)

235-261: build_bipartite_graph uses std::vector for node storage — should use DynArray.

left_nodes and right_nodes are std::vector<typename GT::Node*> but should be DynArray<typename GT::Node*> per project container policy. The same pattern used in build_graph (line 79) should be applied here.

♻️ Proposed fix to use Aleph containers
   template <class GT>
   GT build_bipartite_graph(size_t left_size,
                            size_t right_size,
                            const std::vector<std::pair<size_t, size_t>> & edges_lr)
   {
     GT g;
 
-    std::vector<typename GT::Node *> left_nodes;
-    std::vector<typename GT::Node *> right_nodes;
-    left_nodes.reserve(left_size);
-    right_nodes.reserve(right_size);
+    DynArray<typename GT::Node *> left_nodes;
+    DynArray<typename GT::Node *> right_nodes;
+    left_nodes.reserve(left_size);
+    right_nodes.reserve(right_size);
 
     for (size_t i = 0; i < left_size; ++i)
-      left_nodes.push_back(g.insert_node(static_cast<int>(i)));
+      left_nodes(i) = g.insert_node(static_cast<int>(i));
 
     for (size_t i = 0; i < right_size; ++i)
-      right_nodes.push_back(g.insert_node(static_cast<int>(left_size + i)));
+      right_nodes(i) = g.insert_node(static_cast<int>(left_size + i));
 
     for (const auto & [l, r] : edges_lr)
       {
         if (l >= left_size or r >= right_size)
           continue;
-        g.insert_arc(left_nodes[l], right_nodes[r], 1);
+        g.insert_arc(left_nodes(l), right_nodes(r), 1);
       }
 
     return g;
   }

As per coding guidelines: MUST use Aleph containers when an equivalent exists. DynArray<T> instead of std::vector<T>.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/blossom_test.cc` around lines 235 - 261, Replace the std::vector usage
in build_bipartite_graph: change left_nodes and right_nodes from
std::vector<typename GT::Node*> to DynArray<typename GT::Node*> and use
DynArray's reserve and push_back operations when populating nodes (same pattern
as build_graph). Ensure you reference GT::Node for the element type and update
any includes/imports if needed so DynArray is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/planarity_gephi_nightly.yml:
- Around line 1-6: The workflow name "Planarity Gephi Nightly" and any
artifact/job naming conventions using the "gephi-nightly-*" prefix are
inconsistent with the cron schedule ('30 3 * * 0') which runs weekly; update the
name field to something like "Planarity Gephi Weekly" and rename artifact/job
prefixes from "gephi-nightly-*" to "gephi-weekly-*" (or another
weekly-consistent prefix) so names match the actual cadence while leaving the
existing schedule unchanged.
- Around line 418-422: When calling json.loads(proc.stdout) in the success
branch, wrap the parse in a try/except that catches json.JSONDecodeError (and
optionally ValueError) so non-JSON output from the Ruby validator doesn't raise
an unhandled exception; on parse failure set summary["overall_valid"] = False,
set exit_code = 2 (or preserve the existing non-valid behavior), and log or
include the raw proc.stdout in the diagnostic message so callers can see the
invalid output; update the block around json.loads, proc.stdout,
summary["overall_valid"], and exit_code to implement this handling.
- Around line 509-524: The workflow step passes an empty ALERT_WEBHOOK to the
Ruby notifier, so add a shell guard at the start of the run block to skip
running scripts when ALERT_WEBHOOK is unset or empty; check the ALERT_WEBHOOK
env variable (from the step's env) and exit successfully (or echo a skip
message) if it's blank before invoking planarity_gephi_regression_notify.rb,
ensuring the step truly behaves as an optional webhook.
- Around line 267-268: The code currently calls
urllib.request.urlopen(req_asset, timeout=300) and then
archive_path.write_bytes(resp.read()), which loads the entire archive into
memory; change this to stream the response to disk in chunks instead: open
archive_path for binary writing and iterate reading resp.read(CHUNK_SIZE) (e.g.,
64KB or 1MB) until empty, writing each chunk to the file, or use
shutil.copyfileobj(resp, fileobj, length=CHUNK_SIZE) to avoid buffering the
whole response; keep the same urlopen call and timeout but replace
resp.read()/write_bytes usage with chunked writes to prevent OOM on large
downloads.
- Around line 247-256: The check that currently only rejects assets with
score_asset(ranked[0]) < 0 is too permissive; update the guard to require the
selected asset to be OS-specific by verifying score_asset(ranked[0]) >= 80
(ensuring the +80 OS token was applied) and otherwise raise the same SystemExit
with the assets names; modify the block using ranked, score_asset, assets and
the SystemExit raise to perform this >=80 check before accepting ranked[0].
- Around line 270-277: The tar extraction call using
tarfile.extractall(archive_path, ...) is unsafe and deprecated without an
explicit filter; update the branch handling asset_name.endswith(".tar.gz") so
the TarFile is opened via tarfile.open(archive_path, "r:gz") as tf and call
tf.extractall(ex_dir, filter="data") to enforce the safe data-only extraction
behavior (keep the existing zipfile.extractall for .zip assets and the
SystemExit fallback unchanged); reference tarfile.open and TarFile.extractall to
locate the edit.

In `@Dominators.H`:
- Around line 267-270: The parameter constness in do_compute is misleading
because the function and helpers (dfs, NODE_COUNTER modifications) mutate node
bookkeeping; remove the const_cast and change the signature of do_compute(const
GT & g, Node *root) to accept GT & g, then propagate the same change to all
public methods and free functions that forward graphs (compute_idom, build_tree,
dominance_frontiers, and any related overloads), update callers to pass
non-const GT references, and eliminate the const_cast usage (gptr =
&const_cast<GT &>(g)); ensure NODE_COUNTER writes remain valid against the
non-const GT parameter.
- Around line 530-562: The dominance_frontiers function currently accumulates
duplicates because df is a DynMapTree<Node*, DynList<Node*>> and the walk-up
loop (the loop over pred[b] that calls df.find(vertex[runner]).append(bnode))
may append the same bnode multiple times; change the frontier storage to a set
or prevent duplicates: replace DynList with a set type (e.g., DynSetTree or
DynSet) so df is DynMapTree<Node*, DynSet<Node*>> (or use DynSetTree<Node*,
DynSet<Node*>>), or alternatively, keep DynList but check membership before
append (use df.find(vertex[runner]).contains(bnode) or equivalent) in the inner
loop where runner is walked up; update any return type and consumers of
dominance_frontiers accordingly (symbols: dominance_frontiers, df, pred,
idom_arr, vertex, num_reachable).
- Around line 267-301: The early return in do_compute (after dfs) can leave pred
uninitialized which causes out-of-bounds access in dominance_frontiers; to fix,
allocate and initialize pred (e.g., pred = Array<long>(N, -1L)) along with the
other internal arrays (vertex, par, semi, idom_arr, anc, label_arr) before the
num_reachable <= 1 check so pred is valid for single-node graphs, and/or add a
guard in dominance_frontiers to return immediately if num_reachable <= 1; update
references to pred and ensure idom_arr[0] is still set for the single-node case.
- Around line 714-788: The cache-check in ensure_computed (uses is_computed,
gptr, exit_ptr) is brittle because it compares raw graph pointer (&g) and will
miss logically-equivalent graph copies; change the validity check to compare a
stable graph identity (e.g., a version/id or size/structure fingerprint) or
require callers to pass the same instance and document the precondition; also
document that invert_digraph produces new Arc objects so sa (the arc filter
passed into Lengauer_Tarjan_Post_Dominators and used by dom) must be stateless
or re-bindable—either update class docs to state filters must not rely on
original Arc identity (mention Dft_Show_Arc as safe) or add a step in
ensure_computed to re-map or re-evaluate arc-dependent filters after rev =
invert_digraph(g) using orig_to_rev/rev_to_orig so custom filters that depend on
arc identity are corrected.

In `@Hungarian.H`:
- Around line 492-520: hungarian_max_assignment can still overflow when Promoted
is a signed type equal to long long and a cell equals numeric_limits<long
long>::min(); add a constexpr check and handle that case by negating using a
wider signed intermediate (e.g., __int128) to avoid overflow: inside the loop
that fills negated(i,j) (refer to Promoted, negated, cost.read), use if
constexpr(std::is_signed_v<Promoted>) { auto v =
static_cast<Promoted>(cost.read(i,j)); if (v ==
std::numeric_limits<Promoted>::min()) negated(i,j) =
static_cast<Promoted>(-static_cast<__int128>(v)); else negated(i,j) = -v; } else
{ negated(i,j) = -static_cast<Promoted>(cost.read(i,j)); } ensuring the wider
type is only used as an intermediate to compute the negated value safely.

In `@K_Shortest_Paths.H`:
- Around line 1025-1185: Add full Doxygen for the public API
eppstein_k_shortest_paths and its functor wrapper
Eppstein_K_Shortest_Paths::operator(): include `@brief`, `@param` entries for g,
source, target, k, distance and sa, `@return` describing the
DynList<K_Shortest_Path_Item<...>>, `@throws` for the domain errors (source/target
null) and negative-weight validation, and a `@note` or `@complexity` line with Big-O
time/space bounds mirroring the level of detail used in yen_k_shortest_paths;
also add a short thread-safety statement. Ensure the tags appear immediately
above the template function eppstein_k_shortest_paths and above the operator()
method in class Eppstein_K_Shortest_Paths.

In `@Tests/k_shortest_paths_test.cc`:
- Around line 393-419: Add a new unit test that constructs a small Graph via
make_edges/build_graph containing (1) a self-loop on a spur node and (2)
parallel arcs between the same node pair with different weights, then call
yen_k_shortest_paths<Graph> and eppstein_k_shortest_paths<Graph> from the same
source/target and assert both algorithms return the same K results, that the
self-loop is not chosen as a valid simple path spur, and that parallel edges
appear in the returned path list in deterministic order by total weight (use
EXPECT_EQ/EXPECT_TRUE to check count, ordering, and exact path weights/vertex
sequences). Ensure you reference the same types and functions used elsewhere
(yen_k_shortest_paths, eppstein_k_shortest_paths, make_edges, build_graph,
Graph) so the test integrates with existing tests.

---

Duplicate comments:
In @.github/workflows/planarity_gephi_nightly.yml:
- Around line 236-244: The Windows OS detection conditional using runner_os ==
"Windows" and checks on the variable lower (lower.startswith("win"), "win-" in
lower, "win64" in lower, "win32" in lower) correctly avoids matching "darwin"
and resolves the false-positive; no code change required—please mark this review
as approved/merge-ready and remove any previous darwin-related workarounds that
were introduced to handle the false positive if they exist.

---

Nitpick comments:
In `@Blossom.H`:
- Around line 195-222: The final "return b" in lca(size_t a, size_t b) is only a
safety fallback and should be asserted as unreachable; add a debug assertion
(e.g., assert(false && "lca fallback reached") or a platform-specific
unreachable check) just before the return to document the invariant and help
catch logic bugs at runtime, referencing the lca function and leaving the
fallback return in place for safety in release builds.

In `@Examples/planarity_test_example.cc`:
- Around line 274-289: The current write blocks that create ofstream objects
(fj, fd, fg, fx) and stream the contents (json, dot, graphml, gexf) do not check
whether the files opened or whether the writes succeeded; update each block that
opens json_path, dot_path, graphml_path and gexf_path to verify the stream
opened (e.g., test fj/is_open or operator bool) and check for write errors after
writing (e.g., check fail() or good()), and on error return/throw or log a clear
message including the path and the stream error state so failures are detected
and propagated instead of silently ignored.

In `@Tests/blossom_test.cc`:
- Around line 235-261: Replace the std::vector usage in build_bipartite_graph:
change left_nodes and right_nodes from std::vector<typename GT::Node*> to
DynArray<typename GT::Node*> and use DynArray's reserve and push_back operations
when populating nodes (same pattern as build_graph). Ensure you reference
GT::Node for the element type and update any includes/imports if needed so
DynArray is available.

In `@Tests/dominators_test.cc`:
- Around line 705-718: The ipdom_map helper currently returns a
std::map<int,int>; replace it with Aleph's DynMapTree<int,int> so it matches the
idom_map pattern: change the function return type and local variable m to
DynMapTree<int,int>, insert entries via m.set(key, value) (or the DynMapTree
equivalent) while iterating the DynList of std::pair<DG::Node*,DG::Node*>
(ipdoms) and return m; update all PostDominators test call sites to use
DynMapTree lookup APIs instead of std::map::operator[] or find to accommodate
the container change.
- Around line 686-698: Replace the std::map usage in the dominators test with
the project's DynMapTree<K,V> container: change the variable declaration m from
std::map<std::string,std::string> to DynMapTree<std::string,std::string>, and
when populating it in the loop that iterates idoms via idoms.get_it() use
DynMapTree's insert/assignment API to set m[node->get_info()] = idom ?
idom->get_info() : "none"; leave the EXPECT_EQ checks for m["entry"], m["body"],
and m["exit"] unchanged so the assertions use the DynMapTree instead of
std::map.

In `@Tests/lca_test.cc`:
- Around line 117-131: The recursive dfs in Naive_Tree_Oracle (function dfs) can
overflow for deep/linear trees; replace it with an iterative DFS using an
explicit stack to simulate recursion so parent_, depth_, tin_, tout_, and timer_
are set correctly: push a tuple/state for the root (node, parent, depth,
entering flag), on entering set parent_[u], depth_[u], tin_[u]=timer_++, iterate
adjacency via adj_[u] to push children (skip parent), and on exiting set
tout_[u]=timer_-1; ensure you reference and update timer_, adj_, parent_,
depth_, tin_, and tout_ consistently to preserve current behavior without using
the call stack.

In `@Tests/min_cost_matching_test.cc`:
- Around line 56-60: Many test utilities (build_graph, verify_matching,
normalize_simple_edges, the DP oracle, and test cases) use std::vector,
std::unordered_set, and std::unordered_map; replace these with the project's
Aleph container equivalents (Array/ DynArray for vectors and Aleph hash
containers for unordered_* where equivalents exist) to comply with the coding
guideline. Update types and construction/iteration code in functions
build_graph, verify_matching, normalize_simple_edges, the DP oracle (memo usage
that needs O(1) random access should keep an Array or DynArray as appropriate),
and all test cases to use Array<Cost_Edge>/DynArray<Cost_Edge> instead of
std::vector<Cost_Edge>, and replace std::unordered_set/std::unordered_map usages
with the Aleph hash set/map types; keep std::vector only where no Aleph
equivalent exists and ensure API callsites (emplace/insert, size, iteration,
indexing) are adjusted to the Aleph container methods.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 22, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings and committed to branch graph-round-3 (commit: 7563d584b1917448283750b9550893115cbf7d5a)

Docstrings generation was requested by @lrleon.

The following files were modified:

* `Examples/blossom_example.cc`
* `Examples/centroid_decomposition_example.cc`
* `Examples/dominators_example.cc`
* `Examples/heavy_light_decomposition_example.cc`
* `Examples/hld_example.cc`
* `Examples/hungarian_example.cc`
* `Examples/k_shortest_paths_example.cc`
* `Examples/lca_example.cc`
* `Examples/min_cost_matching_example.cc`
* `Examples/min_mean_cycle_example.cc`
* `Examples/planarity_test_example.cc`
* `Examples/weighted_blossom_example.cc`
* `Tests/ah-functional.cc`
* `Tests/geom_algorithms_test_common.h`

These files were ignored:
* `Tests/ah_concepts_test.cc`
* `Tests/blossom_test.cc`
* `Tests/cut_nodes_test.cc`
* `Tests/dominators_test.cc`
* `Tests/hld_test.cc`
* `Tests/hungarian_test.cc`
* `Tests/k_shortest_paths_test.cc`
* `Tests/lca_test.cc`
* `Tests/min_cost_matching_test.cc`
* `Tests/min_mean_cycle_test.cc`

These file types are not supported:
* `.github/workflows/ci.yml`
* `.github/workflows/planarity_gephi_nightly.yml`
* `Blossom.H`
* `Blossom_Weighted.H`
* `CMakeLists.txt`
* `Dominators.H`
* `Examples/CMakeLists.txt`
* `Examples/cut_nodes_example.C`
* `HLD.H`
* `Hungarian.H`
* `K_Shortest_Paths.H`
* `LCA.H`
* `Min_Cost_Matching.H`
* `Min_Mean_Cycle.H`
* `README.es.md`
* `README.md`
* `Tests/CMakeLists.txt`
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (27)
Examples/dominators_example.cc (1)

161-205: Duplicated frontier-printing logic between sections 4 and 5.

Sections 4 (lines 161–176) and 5 (lines 187–204) iterate over df with nearly identical printing loops. For an example file this is tolerable, but extracting a small lambda (e.g., print_frontier) would reduce the duplication and make both sections easier to follow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/dominators_example.cc` around lines 161 - 205, Duplicate
iteration/printing over the dominance-frontier map `df` appears in the two
blocks that use `for (auto it = df.get_it(); it.has_curr(); it.next_ne())` and
inner `for (auto fi = frontier.get_it(); fi.has_curr(); fi.next_ne())`; refactor
by extracting a small reusable lambda or helper (e.g., `print_frontier`) that
accepts a node and its `frontier` and prints the "{...}" list with the same
comma-separation logic, then replace both duplicated loops (the one under "===
Dominance Frontiers ===" and the one under "=== SSA Phi-Function Placement ===")
to call this helper for each `it.get_curr()` pair; ensure the helper uses the
same calls (`get_info()`, `is_empty()`, `get_it()`, `has_curr()`, `next_ne()`)
so behavior is unchanged.
Examples/planarity_test_example.cc (2)

340-412: Consider extracting the verbose helper-command output into a separate function.

This ~70-line block of cout statements printing validator/render/CI commands obscures the core planarity demonstration. Extracting it (e.g., print_helper_commands(output_dir, graphml_path, gexf_path)) would keep main focused on the graph construction and planarity-test workflow, and make the helper-command catalog easier to maintain independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/planarity_test_example.cc` around lines 340 - 412, Extract the
~70-line cout block into a new helper function like print_helper_commands(const
fs::path& output_dir, const string& graphml_path, const string& gexf_path) and
replace the block in main with a single call to that function; move the derived
paths (render_dir, ci_report, ci_render_report, visual_render_dir,
visual_report, nightly_comparison_json, nightly_alert_md) into the helper so
they are computed locally, keep the same printed strings/formatting, and ensure
the new function is declared before main (or in an appropriate header) so
compilation succeeds.

313-328: File writes are not checked for success — certificate files could silently fail.

Four ofstream objects are opened and written without verifying good()/is_open(). If the directory is unwritable, the example silently prints paths to non-existent files. Consider at least a brief check after writing, consistent with the warning already emitted for create_directories.

Example: minimal write-check helper
+  auto write_file = [](const string & path, const string & content) {
+    ofstream f(path);
+    f << content;
+    if (not f)
+      cout << "  warning: failed to write " << path << '\n';
+  };
+
-  {
-    ofstream fj(json_path);
-    fj << json;
-  }
-  {
-    ofstream fd(dot_path);
-    fd << dot;
-  }
-  {
-    ofstream fg(graphml_path);
-    fg << graphml;
-  }
-  {
-    ofstream fx(gexf_path);
-    fx << gexf;
-  }
+  write_file(json_path, json);
+  write_file(dot_path, dot);
+  write_file(graphml_path, graphml);
+  write_file(gexf_path, gexf);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/planarity_test_example.cc` around lines 313 - 328, The four file
writes using ofstream (fj, fd, fg, fx) open and write to json_path, dot_path,
graphml_path and gexf_path but do not verify success; add a short post-write
check (e.g., ensure stream.is_open() and stream.good() or check after
flush/close) and emit an error/warning if the write failed (consistent with the
existing create_directories warning). Implement this by adding a small helper
(e.g., write_file_or_warn) or inline checks after each write that report the
path and failure reason if the stream is not good, referencing the existing
variable names fj/fd/fg/fx and the path variables
json_path/dot_path/graphml_path/gexf_path so failures are not silently ignored.
Tests/geom_algorithms_test_common.h (2)

94-94: sort3 and polygon_signed_area are missing [[maybe_unused]], inconsistent with every other helper in this namespace.

Both are only reachable from [[maybe_unused]]-annotated callers (canonical_triangles / polygon_area / is_ccw). A translation unit that includes this header but never invokes those callers may emit -Wunused-function warnings for these two.

♻️ Proposed fix
-  void sort3(size_t & a, size_t & b, size_t & c)
+  [[maybe_unused]] void sort3(size_t & a, size_t & b, size_t & c)
-  Geom_Number polygon_signed_area(const Polygon & poly)
+  [[maybe_unused]] Geom_Number polygon_signed_area(const Polygon & poly)

Also applies to: 190-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/geom_algorithms_test_common.h` at line 94, Add the [[maybe_unused]]
attribute to the helper functions sort3 and polygon_signed_area so they match
the other helpers and won't trigger -Wunused-function when their only callers
(canonical_triangles, polygon_area, is_ccw) are unused; update the function
declarations/definitions for sort3 and polygon_signed_area to include
[[maybe_unused]] in their signature.

35-35: Prefer ah_domain_error() over throw std::domain_error; remove <stdexcept>.

throw std::domain_error(...) at line 176 is the only reason <stdexcept> is included. The rest of the codebase uses Aleph's own unconditional error macros; this should be consistent.

♻️ Proposed change
-#include <stdexcept>
-    if (den == 0)
-      throw std::domain_error("circumcenter_of: collinear triangle (zero area)");
+    if (den == 0)
+      ah_domain_error("circumcenter_of: collinear triangle (zero area)");

Based on learnings: "Use unconditional Aleph error macros: ah_domain_error(), ah_runtime_error(), ah_range_error(), ah_invalid_argument()".

Also applies to: 175-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/geom_algorithms_test_common.h` at line 35, Replace the unconditional
throw of std::domain_error with the project's macro ah_domain_error() and remove
the now-unused `#include` <stdexcept>; specifically, locate the throw
std::domain_error(...) usage (around the check at/near lines 175-176) and change
it to call ah_domain_error(...) with the same message, and then delete the
<stdexcept> include since it is no longer needed.
Examples/blossom_example.cc (2)

86-98: Doxygen comment placed between template and function signature — move it before the template line.

Doxygen typically expects the documentation comment immediately before the full declaration (including the template keyword). Placing it between template <class GT> and the function name can cause Doxygen to fail to associate the comment with the function. This pattern repeats at lines 113, 251, and 266.

Proposed fix (example for build_graph)
-  template <class GT>
-  /**
-   * `@brief` Constructs a graph of type `GT` that represents the given scenario.
+  /**
+   * `@brief` Constructs a graph of type `GT` that represents the given scenario.
    ...
-   */
+   */
+  template <class GT>
   GT build_graph(const Scenario & s)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/blossom_example.cc` around lines 86 - 98, Move the Doxygen block so
it sits immediately before the entire templated declaration instead of between
the template header and the function name: for example, place the comment now
above the line "template <class GT>" that precedes GT build_graph(const Scenario
& s). Do the same for the other templated functions that currently have their
Doxygen comments inserted between "template <...>" and the function signature
(i.e., relocate each comment to directly precede its corresponding "template
<...>" line so Doxygen associates the comment with the full declaration).

56-56: std::set could be replaced with DynSetTree per Aleph container guidelines.

std::set<pair<size_t, size_t>> is used in write_tikz (line 189). The coding guidelines require Aleph containers when an equivalent exists. As this is an example file, it's a minor deviation.

As per coding guidelines: "MUST use Aleph containers when an equivalent exists. Standard library containers are ONLY permitted when no Aleph equivalent is available." and "Use DynSetTree<T> instead of std::set<T>".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/blossom_example.cc` at line 56, Replace the
std::set<pair<size_t,size_t>> used in write_tikz with Aleph's
DynSetTree<pair<size_t,size_t>>: change the `#include` for <set> to the Aleph
DynSetTree header, update the container type in write_tikz and any variable
declarations, and adjust calls that manipulate the set (insert, erase,
find/contains, iteration) to use the DynSetTree API (same semantics but use
DynSetTree methods/iterators). Ensure all references to the old std::set symbol
are renamed to DynSetTree to satisfy the Aleph container guideline.
Tests/ah-functional.cc (2)

1631-1645: Test name overstates what is verified — rename to match actual assertion.

The test only checks hash_calls > 0 (i.e., that the custom hash functor is invoked at all). It does not verify that DynSetHash is used internally; the name custom_hash_functor_is_used_when_dynsethash_is_available implies a structural implementation guarantee that the assertion doesn't provide.

Consider renaming to something like custom_hash_functor_is_invoked to avoid overstating the invariant being tested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ah-functional.cc` around lines 1631 - 1645, The test currently named
custom_hash_functor_is_used_when_dynsethash_is_available overstates its check;
rename the TEST second parameter to something like
custom_hash_functor_is_invoked (so the macro becomes TEST(all_unique_overload,
custom_hash_functor_is_invoked)) and update any references/comments to match,
since the body only asserts that the provided hash functor was invoked
(hash_calls > 0) and does not verify use of DynSetHash or other internal
implementation details.

1647-1661: empty_container and single_element tests don't cover the hash+eq overload.

Both tests exercise only all_unique(container, eq) and all_unique(container). The three-argument overload all_unique(container, hash, eq) is not tested with empty or single-element containers, which are the most common edge-case failure points for hash-based implementations.

➕ Suggested additions
 TEST(all_unique_overload, empty_container)
 {
   DynList<int> empty;
   EXPECT_TRUE(all_unique(empty, std::equal_to<int>()));
   EXPECT_TRUE(all_unique(empty));
+  // Hash+eq overload
+  EXPECT_TRUE(all_unique(empty, CaseInsensitiveStringHash(), CaseInsensitiveStringEqual()));
 }

 TEST(all_unique_overload, single_element)
 {
   auto single = build_dynlist<int>(42);
   EXPECT_TRUE(all_unique(single, std::equal_to<int>()));
   EXPECT_TRUE(all_unique(single));
+  // Hash+eq overload
+  auto single_str = build_dynlist<string>("only");
+  EXPECT_TRUE(all_unique(single_str, CaseInsensitiveStringHash(), CaseInsensitiveStringEqual()));
 }

Based on learnings: "Tests must cover edge cases: empty inputs, single element, large inputs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ah-functional.cc` around lines 1647 - 1661, Add tests that exercise the
three-argument overload all_unique(container, hash, eq) for the empty and
single-element edge cases: update the tests named
all_unique_overload::empty_container and all_unique_overload::single_element
(which currently call all_unique(empty, std::equal_to<int>()) and
all_unique(single, std::equal_to<int>())) to also call the hash+eq overload
(e.g., pass a std::hash<int> or equivalent hash functor along with
std::equal_to<int>), ensuring both EXPECT_TRUE assertions include the
three-argument form using the same DynList<int> instances built with
build_dynlist<int>().
Examples/lca_example.cc (3)

75-78: Prefer std::array over C-style arrays.

Both NODE_LABELS (Line 75) and results (Line 273) use C-style arrays of known compile-time size. As per coding guidelines, std::array should be used instead.

♻️ Proposed refactors
-  static const char * const NODE_LABELS[NODES] = {
+  static constexpr std::array<const char *, NODES> NODE_LABELS = {
       "CEO", "CTO", "CFO", "Platform", "Data",
       "Accounting", "Legal", "SRE", "ML"
-  };
+  };
-  const Backend_Result results[] = {list_result, sgraph_result, agraph_result};
+  const std::array<Backend_Result, 3> results = {list_result, sgraph_result, agraph_result};

As per coding guidelines: "Use std::array instead of C-style arrays when size is known at compile-time."

Also applies to: 273-273

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/lca_example.cc` around lines 75 - 78, Replace the C-style arrays
with std::array: include <array> and change NODE_LABELS to std::array<const
char*, NODES> NODE_LABELS = { ... } and likewise change the results C-style
array (referenced at symbol results) to an appropriate std::array<T, SIZE> with
the same element type and compile-time size; update any code that takes the raw
pointer/decays (e.g., passing to functions expecting T* or using sizeof tricks)
to use results.data() or results.size() as needed. Ensure the element type
matches (const char* or the actual type used for results) and keep the original
initializer values.

109-123: Doxygen comment should precede the template declaration, not follow it.

The /** @brief ... */ block at Line 110 comes after template <class GT> (Line 109). Doxygen conventionally expects the documentation comment to immediately precede the full declaration, including the template preamble, to ensure correct association.

♻️ Proposed fix
-  template <class GT>
-  /**
-   * `@brief` Builds a small rooted tree ...
-   * ...
-   */
-  Backend_Result run_backend(const char * backend_name)
+  /**
+   * `@brief` Builds a small rooted tree ...
+   * ...
+   */
+  template <class GT>
+  Backend_Result run_backend(const char * backend_name)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/lca_example.cc` around lines 109 - 123, The Doxygen comment block is
placed after the template declaration so it won't associate with the full
template function; move the entire comment (the /** ... */ block) so it
immediately precedes the template declaration for run_backend (i.e., place the
comment above "template <class GT>" so the documentation attaches to the
templated function Backend_Result run_backend(const char * backend_name));
ensure the comment still references GT and backend_name as before.

249-291: main() always returns 0 regardless of validation failures — CI cannot detect regressions.

All the correctness flags (deterministic_ok, engines_agree, checksum_equal, cross_backend_checksum_ok) are printed but never reflected in the exit code. A mis-implemented LCA engine would produce a silent pass in any automated pipeline.

♻️ Proposed fix
+  const bool all_ok = cross_backend_checksum_ok and
+      std::ranges::all_of(results, [](const auto & r) {
+          return r.deterministic_ok and r.engines_agree and r.checksum_equal;
+      });
+
+  if (not all_ok)
+    {
+      std::cerr << "FAILURE: one or more validation checks failed.\n";
+      return 1;
+    }
+
   return 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/lca_example.cc` around lines 249 - 291, main() currently always
returns 0 even when validation fails; change it to return non-zero on any failed
check by evaluating the Backend_Result flags and cross_backend_checksum_ok:
after computing list_result, sgraph_result, agraph_result and
cross_backend_checksum_ok, compute a boolean failure =
!list_result.deterministic_ok || !list_result.engines_agree ||
!list_result.checksum_equal || !sgraph_result.deterministic_ok ||
!sgraph_result.engines_agree || !sgraph_result.checksum_equal ||
!agraph_result.deterministic_ok || !agraph_result.engines_agree ||
!agraph_result.checksum_equal || !cross_backend_checksum_ok (or equivalent
aggregated check over the results array), and return failure ? 1 : 0 from main()
so CI fails when any correctness flag (deterministic_ok, engines_agree,
checksum_equal, cross_backend_checksum_ok) is false.
Examples/min_cost_matching_example.cc (1)

314-324: Same Doxygen misplacement as the weighted example — comment floats between } and main().

The } /** on Line 314 closes the anonymous namespace and starts a Doxygen block that Doxygen won't associate with main(). Move it directly above int main().

Proposed fix
-} /**
- * `@brief` Runs example demonstrations for minimum-cost matching ...
- * `@return` int 0 on success.
- */
-
+} // namespace
 
+/**
+ * `@brief` Runs example demonstrations for minimum-cost matching and perfect matching.
+ * `@return` int 0 on success.
+ */
 int main()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_cost_matching_example.cc` around lines 314 - 324, The Doxygen
block is misplaced after the anonymous-namespace closing brace; move the comment
so it immediately precedes the definition of int main() (i.e., place the /** ...
*/ Doxygen comment directly above the main() function declaration) instead of
sitting on the same line as the closing brace of the anonymous namespace to
ensure Doxygen associates the documentation with main().
Examples/weighted_blossom_example.cc (2)

363-373: Doxygen block for main() is misplaced between namespace brace and function.

The } /** on Line 363 closes the anonymous namespace and immediately starts a Doxygen block. This block is syntactically floating — Doxygen won't attach it to main(). Move it directly above int main().

Proposed fix
-} /**
- * `@brief` Runs example scenarios demonstrating maximum-weight matching ...
- * `@return` int Exit status code; `0` on successful completion.
- */
-
+} // namespace
 
+/**
+ * `@brief` Runs example scenarios demonstrating maximum-weight matching and writes TikZ visualizations.
+ *
+ * `@return` int Exit status code; `0` on successful completion.
+ */
 int main()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/weighted_blossom_example.cc` around lines 363 - 373, The Doxygen
comment block currently sits after the anonymous-namespace closing brace (`}`)
and therefore isn't attached to `int main()`; move the entire Doxygen block (the
/** ... */ describing main and its return) so it immediately precedes the `int
main()` definition (not after the namespace `}`), ensuring the block is directly
above the `main()` signature so Doxygen associates the documentation with the
`main()` function.

100-126: Doxygen comment placement: should precede template line.

The template <class GT> declaration on Line 100 precedes the Doxygen block (Lines 101–112), meaning Doxygen will associate the comment with the template parameter list rather than the function. The same pattern repeats at Lines 129/284. Move the /** ... */ block above the template line.

Proposed fix
+  /**
+   * `@brief` Construct a graph of type GT from a Scenario.
+   * ...
+   */
   template <class GT>
-  /**
-   * `@brief` Construct a graph of type GT from a Scenario.
-   * ...
-   */
   GT build_graph(const Scenario & s)

Apply the same reorder to solve_case (Line 129) and print_backend (Line 284).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/weighted_blossom_example.cc` around lines 100 - 126, The Doxygen
comment blocks are placed after the template declarations so they attach to the
template parameter list instead of the function; move each /** ... */ block so
it immediately precedes the corresponding template line for the functions
build_graph, solve_case, and print_backend so the comment is associated with the
function template (ensure the comment sits above "template <...>" for each of
those symbols).
Examples/min_mean_cycle_example.cc (2)

147-162: } /** mixes namespace closing brace and main's Doxygen block on one line.

Minor readability nit — add a blank line (and optionally an explicit end-of-namespace comment) between the two.

♻️ Suggested split
-} /**
- * `@brief` Example program demonstrating Karp's minimum mean cycle algorithm on Aleph digraphs.
+} // end anonymous namespace
+
+/**
+ * `@brief` Example program demonstrating Karp's minimum mean cycle algorithm on Aleph digraphs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` around lines 147 - 162, The closing brace
and the Doxygen block for main are mashed together ("} /**") which hurts
readability; split them so the namespace/class closing brace stands alone on its
own line, add a blank line before the Doxygen comment that documents main, and
optionally append an explicit end-of-namespace comment (e.g., "} // namespace
<name>") to the brace; update the area around the end of the namespace and the
start of int main() (symbols: the closing brace before the Doxygen block and the
function main) accordingly.

58-72: Hide_Arc name is semantically inverted; struct lacks @brief.

operator() returns true when arc != blocked — i.e., for arcs to include/keep, not arcs to hide. Readers expecting a "should-hide?" predicate will find the logic reversed. A name like Arc_Visible or Not_Blocked better matches the return-value convention. Also, the struct itself has no Doxygen @brief; only its operator() is documented.

♻️ Suggested rename and struct-level doc
+  /**
+   * `@brief` Arc-filter predicate: returns true for arcs that should be
+   *        included in the traversal (i.e., arcs that are not blocked).
+   */
-  struct Hide_Arc
+  struct Arc_Visible
   {
     Arc * blocked = nullptr;
 
     /**
-     * `@brief` Predicate that checks whether an arc is not the blocked arc.
+     * `@brief` Returns true if `@p` arc is not the blocked arc.
      *
      * `@param` arc Pointer to the arc to test; may be nullptr.
-     * `@return` `true` if `arc` is not equal to the predicate's `blocked` arc, `false` otherwise.
+     * `@return` `true` if `@p` arc should be included (is not the blocked arc).
      */
     bool operator()(Arc * arc) const noexcept
     {
       return arc != blocked;
     }
   };

and update the call site:

-      karp_minimum_mean_cycle<Graph, Dft_Dist<Graph>, Hide_Arc>(
-          g, Dft_Dist<Graph>(), Hide_Arc{blocked});
+      karp_minimum_mean_cycle<Graph, Dft_Dist<Graph>, Arc_Visible>(
+          g, Dft_Dist<Graph>(), Arc_Visible{blocked});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` around lines 58 - 72, The predicate
struct Hide_Arc is semantically inverted: operator()(Arc * arc) returns true
when arc != blocked (i.e., arc is visible), so rename the struct to a name that
reflects that behavior (e.g., Arc_Visible or Not_Blocked) and add a Doxygen
`@brief` on the struct itself describing that it tests whether an arc is not the
blocked arc; then update all call sites that construct or refer to Hide_Arc to
use the new name (and keep operator() implementation as-is) so the predicate
name matches its return-value convention.
Examples/k_shortest_paths_example.cc (2)

161-196: Prefer std::format over std::cout stream expressions per coding guidelines.

The output section uses raw std::cout << with string concatenation, while the guidelines specify "std::format for type-safe string formatting (prefer over printf/streams)". Consider replacing the stream expressions with std::format/std::print calls for consistency with the project style.

♻️ Example replacement for lines 142–145
-        cout << "  #" << rank
-             << "  cost=" << item.total_cost
-             << "  path=" << path_to_string(item.path)
-             << '\n';
+        cout << std::format("  #{:}  cost={:}  path={:}\n",
+                            rank, item.total_cost, path_to_string(item.path));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/k_shortest_paths_example.cc` around lines 161 - 196, Replace the raw
std::cout stream expressions in main() (the lines printing header and
"source=..., target=..., k=...") with type-safe formatting calls using
std::format or std::print; specifically, construct a single formatted string for
the header and a single formatted string for the source/target/k line (using
scenario.source, scenario.target, scenario.k) and call std::print or output the
formatted std::string instead of chaining operator<< on std::cout, keeping
print_results("Yen (loopless/simple paths)", yen) and
print_results("Eppstein-style API (general, may include cycles)", epp)
unchanged.

124-141: print_results template uses a hardcoded concrete type — either de-templatize or use the list's element type.

template <class Result_List> implies the function is generic over the container, but line 141 binds directly to Result_Item (a concrete alias for K_Shortest_Path_Item<Graph, long long>). Any instantiation with a different element type silently fails to compile at that binding, and the template parameter adds no real generality.

Additionally, per coding guidelines, template parameters should be constrained with concepts rather than left unconstrained.

♻️ Option A — Remove the template (both callers pass the same list type)
- template <class Result_List>
  void print_results(const string & title,
-                    const Result_List & results)
+                    const DynList<Result_Item> & results)
♻️ Option B — Make the element type a second template parameter and add a concept
- template <class Result_List>
+ template <typename Item, typename Result_List>
+   requires requires (Result_List l) { { l.is_empty() } -> std::convertible_to<bool>; }
  void print_results(const string & title, const Result_List & results)
  {
     ...
-    const Result_Item & item = it.get_curr();
+    const Item & item = it.get_curr();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/k_shortest_paths_example.cc` around lines 124 - 141, print_results
is declared as template<class Result_List> but binds a concrete type via "const
Result_Item & item = it.get_curr()", which breaks generality; either remove the
template and use the concrete list type (de-templatize) since both callers use
the same list, or make the element type explicit and constrain it with a
concept: change the signature to accept the list's element type (e.g.,
template<class Result_List, class Result_Item> or template<class Result_List>
with typename Element = typename Result_List::value_type) and add a concept
requiring an Iterator with has_curr()/next_ne()/get_curr() and an element
exposing total_cost and path; replace the hardcoded `Result_Item` binding with
the deduced element type (or typename Result_List::value_type) and add the
concept in the template parameter list so callers and compilation are correct
(update print_results signature and any callers accordingly).
Examples/heavy_light_decomposition_example.cc (3)

160-162: C-style arrays — prefer std::array per project guidelines

labels, the anonymous queries struct array, ordered_nodes (once added) — all have compile-time-known sizes.

♻️ Example change for the labels array
-  const char * labels[] = {
-      "Central", "North", "West", "South", "N-A",
-      "N-B", "S-A", "S-A1", "S-A2"};
+  constexpr std::array<const char *, 9> labels = {
+      "Central", "North", "West", "South", "N-A",
+      "N-B", "S-A", "S-A1", "S-A2"};

As per coding guidelines: "Use std::array instead of C-style arrays when size is known at compile-time."

Also applies to: 178-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/heavy_light_decomposition_example.cc` around lines 160 - 162,
Replace C-style arrays with std::array for all compile-time sized collections:
change the const char* labels[] to std::array<const char*, N> (use the actual
element count), convert the anonymous queries struct array to
std::array<QueryType, M> (use the concrete struct type and element count), and
make ordered_nodes a std::array<NodeType, K> when its size is known; include
<array> and update initializers to use brace-enclosed lists or CTAD as
appropriate so construction and usages (e.g., indexing, size()) keep working
with the existing functions and code paths referencing labels, the queries
array, and ordered_nodes.

45-49: Non-standard whitespace in preprocessor directives

Every other file in this PR (including hld_example.cc) uses #include without a space. The space is technically tolerated by GCC/Clang but is non-idiomatic and inconsistent.

✂️ Proposed fix
-# include <Tree_Decomposition.H>
-# include <tpl_graph.H>
-
-# include <cassert>
-# include <cstdio>
+#include <Tree_Decomposition.H>
+#include <tpl_graph.H>
+
+#include <cassert>
+#include <cstdio>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/heavy_light_decomposition_example.cc` around lines 45 - 49, The
preprocessor directives in Examples/heavy_light_decomposition_example.cc use a
non-idiomatic space after the '#' (e.g. "# include <Tree_Decomposition.H>", "#
include <tpl_graph.H>", "# include <cassert>", "# include <cstdio>"); update
each to the standard form ("#include <...>") so they match the style used
elsewhere (like hld_example.cc) and avoid inconsistent formatting across the PR.

171-242: Prefer std::format / std::print over std::printf

The coding guidelines specify: "Use std::format for type-safe string formatting (prefer over printf/streams)". All std::printf calls in this file could be replaced with std::print (C++23) or std::cout << std::format(...) (C++20).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/heavy_light_decomposition_example.cc` around lines 171 - 242,
Replace all uses of std::printf in Examples/heavy_light_decomposition_example.cc
with type-safe C++ formatting/printing: use std::print (C++23) or std::cout <<
std::format(...) (C++20). Update the include directives (include <format> and/or
<iostream>) and convert each format string to std::format-style placeholders
(e.g. "id={}  {}  pos={}" etc.) for the blocks that print node mapping, "Path
maintenance cost queries", subtree totals, budget changes, per-segment messages
inside the hld.for_each_path_segment lambda, and the final messages; also
replace the numeric format/width assumptions from "%zu"/"%d"/"%-18s"/"%-8s" to
appropriate std::format formatting (e.g. {:zu} -> {}, align/width with {:<18},
etc.), and ensure the build uses the required C++ standard when choosing
std::print vs std::format.
Examples/hld_example.cc (2)

181-182: const should be constexpr for consistency with Scenario 1

Scenario 1 (line 106) correctly uses constexpr Query queries[]. Scenario 2 uses const instead, without reason.

♻️ Proposed fix
-    const Query queries[] = {{6, 5}, {4, 3}, {6, 3}, {1, 2}};
+    constexpr Query queries[] = {{6, 5}, {4, 3}, {6, 3}, {1, 2}};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/hld_example.cc` around lines 181 - 182, The queries array in
Scenario 2 is declared with const but should be constexpr for consistency with
Scenario 1; update the declaration of the array named queries (and any related
immutable test data in the same scope) from const Query queries[] to constexpr
Query queries[] so the compiler can evaluate it at compile time, matching the
existing constexpr usage in the other scenario and ensuring identical semantics
for Query (struct Query { size_t u, v; }).

98-100: C-style arrays — prefer std::array per project guidelines

names, clearance, bandwidth, and taxes arrays all have compile-time-known sizes across all three scenarios.

♻️ Example change for Scenario 1
-    const char * names[] = {"CEO", "CTO", "CFO", "Eng", "Data",
-                            "Acct", "Legal", "ML"};
-    const int clearance[] = {5, 4, 3, 2, 3, 1, 2, 3};
+    constexpr std::array<const char *, 8> names = {"CEO", "CTO", "CFO", "Eng", "Data",
+                                                    "Acct", "Legal", "ML"};
+    constexpr std::array<int, 8> clearance = {5, 4, 3, 2, 3, 1, 2, 3};

As per coding guidelines: "Use std::array instead of C-style arrays when size is known at compile-time."

Also applies to: 175-176, 244-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/hld_example.cc` around lines 98 - 100, Replace the C-style arrays
(names, clearance, bandwidth, taxes) with std::array instances of the
appropriate compile-time size and element type, add `#include` <array> if missing,
keep them const (or constexpr) as before, and update any code that treats them
as pointers (e.g., sizeof/iteration) to use .size() and range-for or indexed
access so the semantics remain identical; specifically change the declarations
for names and clearance (and the other scenario occurrences) to std::array<const
char*, N> and std::array<int, N> respectively, preserving the values and
constness.
Examples/centroid_decomposition_example.cc (3)

134-135: Defensively initialize label_by_id and node_by_id before the mapping loop.

Array<T>::create(n) likely leaves elements uninitialised. For an 11-node graph all 11 IDs are covered, so this is not a live bug today. However, if n ever exceeds NUM_NODES (e.g., when the graph backend assigns synthetic sentinel IDs), any unset slot accessed in a printf or hld call becomes UB.

♻️ Proposed defensive initialization
   auto label_by_id = Array<const char *>::create(n);
   auto node_by_id  = Array<Node *>::create(n);
 
+  for (size_t i = 0; i < n; ++i)
+    {
+      label_by_id(i) = "(unknown)";
+      node_by_id(i)  = nullptr;
+    }
+
   for (int i = 0; i < NUM_NODES; ++i)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/centroid_decomposition_example.cc` around lines 134 - 135, The
arrays label_by_id and node_by_id are left uninitialized by Array<T>::create(n),
so before the mapping loop set every slot to safe defaults (e.g., label_by_id[i]
= "" or a sentinel const char* and node_by_id[i] = nullptr) to avoid UB when
later printed or passed to hld; update the code around the Array<const char
*>::create and Array<Node *>::create usage (labels and node_by_id) to initialize
all n entries (via a fill/for loop or an Array creation helper) so every index
is valid even if n grows.

126-132: Prefer std::array over C-style arrays and derive size from the array itself.

nodes_arr and village_label are C-style arrays with compile-time-known sizes; the guidelines require std::array. Additionally, constexpr int NUM_NODES = 11 duplicates the array length as a magic number — std::size(nodes_arr) (or std::ssize) would remove the redundancy and prevent divergence.

♻️ Proposed refactor
-  Node * nodes_arr[] = { n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10 };
-  const char * village_label[] = {
-      "Village-0",  "Village-1",  "Village-2",  "Village-3",  "Village-4",
-      "Village-5",  "Village-6",  "Village-7",  "Village-8",  "Village-9",
-      "Village-10"
-  };
-  constexpr int NUM_NODES = 11;
+  const std::array<Node *, 11> nodes_arr = { n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10 };
+  const std::array<const char *, 11> village_label = {
+      "Village-0",  "Village-1",  "Village-2",  "Village-3",  "Village-4",
+      "Village-5",  "Village-6",  "Village-7",  "Village-8",  "Village-9",
+      "Village-10"
+  };
+  constexpr size_t NUM_NODES = std::size(nodes_arr);

As per coding guidelines: "Use std::array instead of C-style arrays when size is known at compile-time."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/centroid_decomposition_example.cc` around lines 126 - 132, Replace
the C-style arrays nodes_arr and village_label with std::array (e.g.,
std::array<Node*, N> and std::array<const char*, N>) and remove the hard-coded
constexpr int NUM_NODES; instead derive the count from the array (e.g., using
nodes_arr.size() or std::size(nodes_arr)) wherever NUM_NODES was used; update
any code that indexes or iterates (references to nodes_arr, village_label, and
NUM_NODES) to use the std::array methods and the computed size.

195-233: Replace std::printf calls with std::format/std::print per project guidelines.

All diagnostic output uses std::printf. The coding guidelines prefer std::format (C++20) for type-safe string formatting. Even in example files, consistency with the rest of the codebase is valuable.

Example replacement for the centroid-root line:

♻️ Example replacement
-  std::printf("=== Emergency Response Network (Centroid Decomposition) ===\n\n");
-  std::printf("Centroid root: %s (id=%zu)\n\n",
-              label_by_id(cd.centroid_root_id()),
-              cd.centroid_root_id());
+  std::print("=== Emergency Response Network (Centroid Decomposition) ===\n\n");
+  std::print("Centroid root: {} (id={})\n\n",
+             label_by_id(cd.centroid_root_id()),
+             cd.centroid_root_id());

(std::print is C++23; for strict C++20 use std::cout << std::format(...). Add #include <print> or #include <format> accordingly.)

As per coding guidelines: "Use std::format for type-safe string formatting (prefer over printf/streams)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/centroid_decomposition_example.cc` around lines 195 - 233, The
example uses C-style std::printf calls; update these to the project-preferred
C++ formatted output (use std::print if the project targets C++23, or std::cout
<< std::format(...) for C++20). Replace each printf invocation around centroid
output, ancestor loop, activation messages and nearest-result prints
(references: cd, label_by_id, cd.for_each_centroid_ancestor_id, activate, query,
brute_query, Node pointers n4,n5,n7,n10,n2,n6,n9,n10) with the chosen std::print
or std::format/std::cout form and add the appropriate header (<print> or
<format>) at the top of the file.

Refactored test examples to use DynArray and DynSetTree for consistency.
Updated K_Shortest_Paths.H with detailed Doxygen documentation.
Introduced Ruby scripts for CI batch visual diff and validation of planarity certificates, enhancing testing capabilities.
Adjusted CI workflow to include Ruby and update visual golden diff for planarity.
Copilot AI review requested due to automatic review settings February 22, 2026 23:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 74 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Tests/k2tree_test.cc (1)

462-462: ⚠️ Potential issue | 🟡 Minor

Trivially-true assertion provides no test value

result.size() returns size_t (unsigned), so EXPECT_GE(result.size(), 0) is always satisfied and catches nothing. Either remove it or replace it with a meaningful assertion, e.g. comparing the result count against a brute-force count over the inserted points.

🛠️ Suggested fix
-      // Just verify it doesn't crash
-      EXPECT_GE(result.size(), 0);
+      // Verify result count does not exceed total inserted points
+      EXPECT_LE(result.size(), static_cast<size_t>(1000));

Or remove the assertion entirely and add a comment that the loop is a crash-safety smoke test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/k2tree_test.cc` at line 462, The EXPECT_GE(result.size(), 0) assertion
is trivially true because result.size() is unsigned; replace or remove it:
either compute an expected_count via a brute-force scan of the inserted points
and assert EXPECT_EQ(result.size(), expected_count) (or EXPECT_GE/EXPECT_LE with
known bounds), or remove the assertion and add a comment that the surrounding
loop is a crash-safety smoke test; locate and update the
EXPECT_GE(result.size(), 0) line and the surrounding test logic that produces
result to implement the meaningful comparison.
Tests/geom_algorithms_test_boolean_3d_serializer_aabb_misc.cc (1)

1604-1654: ⚠️ Potential issue | 🟠 Major

Tests ConvexPolygonOffsetCollinearConsecutiveVertices and ConvexPolygonOffsetLargeInwardOffset are active but expected to fail CI.

Both tests assert correct behavior while their inline comments claim the implementation produces wrong results:

  • Test at line 1630 expects offset_area < orig_area, but comment states impl produces area > 100
  • Test at line 1652 expects result.size() < 3, but comment states impl produces non-degenerate polygon

These tests must be either disabled or rewritten as regression guards:

Option A: Disable until fixed
-TEST_F(GeomAlgorithmsTest, ConvexPolygonOffsetCollinearConsecutiveVertices)
+TEST_F(GeomAlgorithmsTest, DISABLED_ConvexPolygonOffsetCollinearConsecutiveVertices)
-TEST_F(GeomAlgorithmsTest, ConvexPolygonOffsetLargeInwardOffset)
+TEST_F(GeomAlgorithmsTest, DISABLED_ConvexPolygonOffsetLargeInwardOffset)
Option B: Assert buggy behavior as regression guard
-  EXPECT_LT(offset_area, orig_area)
+  EXPECT_GE(offset_area, orig_area) // TODO: collinear vertex handling bug
-  EXPECT_LT(result.size(), 3u)
+  EXPECT_GE(result.size(), 3u) // TODO: half-plane intersection bug with large offsets
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/geom_algorithms_test_boolean_3d_serializer_aabb_misc.cc` around lines
1604 - 1654, The two tests ConvexPolygonOffsetCollinearConsecutiveVertices and
ConvexPolygonOffsetLargeInwardOffset assert correct behavior but are known to
fail; either disable them by renaming the test cases to prefix DISABLED_ (e.g.,
DISABLED_ConvexPolygonOffsetCollinearConsecutiveVertices,
DISABLED_ConvexPolygonOffsetLargeInwardOffset) so CI skips them, or convert them
into regression-guard checks that assert the current buggy behavior (e.g.,
replace EXPECT_LT(offset_area, orig_area) with EXPECT_GT(offset_area, orig_area)
in ConvexPolygonOffsetCollinearConsecutiveVertices and replace
EXPECT_LT(result.size(), 3u) with EXPECT_GE(result.size(), 3u) in
ConvexPolygonOffsetLargeInwardOffset) and add comments referencing the bug id;
pick one approach consistently and keep test names (or comments) indicating that
these are temporary until ConvexPolygonOffset is fixed.
🧹 Nitpick comments (12)
Examples/planarity_test_example.cc (1)

180-448: main is ~270 lines with four inline demo blocks — consider extracting helpers for readability.

Each demo block (planar sample, dense sample, K3,3, digraph) is self-contained and could be a named function (e.g., demo_planar_sample, demo_k33_certificate). This would reduce cognitive load and make it easier to add/remove demos. Low priority given this is an example file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/planarity_test_example.cc` around lines 180 - 448, main is long
because it contains four large inline demo blocks; extract each block into its
own helper function (e.g., demo_planar_sample(UGraph&, const
Planarity_Test_Options&, const fs::path&), demo_dense_sample(...),
demo_k33_certificate(const fs::path&, const Planarity_Test_Options&),
demo_digraph_sample(DGraph&, const Planarity_Test_Options&)) and move the
corresponding logic (graph construction, planarity_test calls, printing,
certificate file writing and validation) into those functions, preserving use of
symbols like Planarity_Test_Options, strict_lr_opts, planarity_test,
nonplanar_certificate_to_json/dot/graphml/gexf, validate_nonplanar_certificate,
planar_geometric_drawing, planar_dual_metadata; make each helper take only the
inputs it needs (output_dir and opts where appropriate), return void (or a small
result struct if needed), and replace the inline blocks in main with simple
calls to these helpers to improve readability.
Examples/hungarian_example.cc (3)

137-141: ha should be const for consistency with example_basic_assignment

ha is not modified after construction. Marking it const aligns with the pattern in example_basic_assignment (line 57) and satisfies "Mark all non-modifying member functions as const" / "Use const for variables that don't change after initialization."

♻️ Proposed fix
-  Hungarian_Assignment<int> ha({
+  const Hungarian_Assignment<int> ha({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/hungarian_example.cc` around lines 137 - 141, The local variable ha
(constructed as Hungarian_Assignment<int> ha({...})) is not modified after
construction—change its declaration to const Hungarian_Assignment<int> ha({...})
to match the pattern used in example_basic_assignment and to signal
immutability; locate the Hungarian_Assignment<int> ha construction and prefix it
with const.

104-107: Replace C-style 2D array with std::array

int data[3][3] is a C-style array with a compile-time-known size.

♻️ Proposed refactor
-  int data[3][3] = {{10, 5, 13}, {3, 9, 18}, {10, 6, 12}};
-  for (size_t i = 0; i < 3; ++i)
-    for (size_t j = 0; j < 3; ++j)
-      mat(i, j) = data[i][j];
+  using Row3 = std::array<int, 3>;
+  constexpr std::array<Row3, 3> data = {{{10, 5, 13}, {3, 9, 18}, {10, 6, 12}}};
+  for (size_t i = 0; i < data.size(); ++i)
+    for (size_t j = 0; j < data[i].size(); ++j)
+      mat(i, j) = data[i][j];

As per coding guidelines: "Use std::array instead of C-style arrays when size is known at compile-time."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/hungarian_example.cc` around lines 104 - 107, Replace the C-style
array 'data' with a std::array to follow guidelines: change the declaration of
data to std::array<std::array<int, 3>, 3> data =
{{{{10,5,13}},{{3,9,18}},{{10,6,12}}}}; (or equivalent nested std::array
initializer), add `#include` <array> if missing, and keep the existing loops that
assign mat(i, j) = data[i][j]; so only the type of 'data' and its initializer
change while preserving its indexing and use in the mat population code.

57-80: Cost matrix is defined twice; prefer std::array over C-style array

The cost data appears identically at lines 58–62 (inside the constructor call) and again at lines 71–76 as a raw C-style int[4][4]. A single constexpr aggregate can serve both purposes, eliminating the duplication and the drift risk. Additionally, per coding guidelines, std::array should replace C-style arrays when the size is known at compile time.

♻️ Proposed refactor
-  const Hungarian_Assignment<int> ha({
-    {82, 83, 69, 92},
-    {77, 37, 49, 92},
-    {11, 69,  5, 86},
-    { 8,  9, 98, 23}
-  });
-
-  cout << "Optimal total cost: " << ha.get_total_cost() << endl;
-  cout << "Assignments:" << endl;
-  for (auto [r, c] : ha.get_assignments())
-    cout << "  Worker " << r << " -> Task " << c << endl;
-  cout << endl;
-
-  // Show individual costs
-  constexpr int costs[4][4] = {
-    {82, 83, 69, 92},
-    {77, 37, 49, 92},
-    {11, 69,  5, 86},
-    { 8,  9, 98, 23}
-  };
-  cout << "Detailed:" << endl;
-  for (auto [r, c] : ha.get_assignments())
-    cout << "  Worker " << r << " -> Task " << c
-         << " (cost " << costs[r][c] << ")" << endl;
+  using Row4 = std::array<int, 4>;
+  constexpr std::array<Row4, 4> costs = {{
+    {82, 83, 69, 92},
+    {77, 37, 49, 92},
+    {11, 69,  5, 86},
+    { 8,  9, 98, 23}
+  }};
+
+  const Hungarian_Assignment<int> ha(costs);   // assuming the ctor accepts a range/array
+
+  cout << "Optimal total cost: " << ha.get_total_cost() << endl;
+  cout << "Assignments:" << endl;
+  for (auto [r, c] : ha.get_assignments())
+    cout << "  Worker " << r << " -> Task " << c << endl;
+  cout << endl;
+
+  cout << "Detailed:" << endl;
+  for (auto [r, c] : ha.get_assignments())
+    cout << "  Worker " << r << " -> Task " << c
+         << " (cost " << costs[r][c] << ")" << endl;

As per coding guidelines: "Use std::array instead of C-style arrays when size is known at compile-time" and "Refactor duplicated code into reusable functions or templates."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/hungarian_example.cc` around lines 57 - 80, The cost matrix is
duplicated as a literal and a C-style int[4][4]; define a single constexpr
std::array<std::array<int,4>,4> (e.g. constexpr std::array<std::array<int,4>,4>
cost = {{ {...}, {...}, {...}, {...} }}), use that same constexpr when
constructing Hungarian_Assignment<int> (pass cost) and use cost[r][c] for the
detailed output; replace the C-style costs array with this std::array and update
references to ha, Hungarian_Assignment, and costs accordingly so there is a
single compile-time source of truth.
Examples/min_mean_cycle_example.cc (3)

144-144: Prefer auto * over the explicit Arc * type.

Arc * arc = it.get_curr(); can use auto * to let the type be deduced, which is consistent with the project's C++20 style guidelines and removes the redundant explicit type that must be kept in sync with the iterator's value type.

♻️ Proposed fix
-        Arc * arc = it.get_curr();
+        auto * arc = it.get_curr();

As per coding guidelines: "Use auto for type deduction."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` at line 144, Replace the explicit pointer
declaration "Arc * arc = it.get_curr();" with type-deducing auto to follow
project C++20 style: use "auto * arc = it.get_curr();" (i.e., change the
declaration in Examples/min_mean_cycle_example.cc where Arc, arc and
it.get_curr() are used) so the compiler deduces the pointer type from the
iterator's get_curr() return type.

156-168: Namespace closing brace and Doxygen comment are fused on the same line.

Line 156 reads } /**, placing the anonymous namespace closing brace immediately before the @brief block for main(). While valid C++, it is visually confusing and unusual. Separate them with a blank line for clarity.

♻️ Proposed fix
-} /**
+}
+
+/**
  * `@brief` Example program demonstrating Karp's minimum mean cycle algorithm on Aleph digraphs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` around lines 156 - 168, The anonymous
namespace closing brace '}' is fused with the Doxygen comment for main (the line
starting with "} /**"), which is confusing; edit the closing brace for the
anonymous namespace (the lone '}') so it sits on its own line followed by a
blank line, then keep the Doxygen block (/** ... `@brief` ... */) for main()
starting on the next line; this affects the anonymous namespace closing token
and the main() Doxygen comment near the end of
Examples/min_mean_cycle_example.cc.

120-122: Prefer ah_fatal_error() over bare abort().

abort() bypasses the Aleph error-reporting infrastructure. The coding guidelines mandate ah_fatal_error() for unrecoverable fatal conditions in all *.{H,cc} files.

♻️ Proposed fix
+#include <ah-errors.H>
 
     cerr << "Sanity check failed: witness mean " << witness_mean
          << " != minimum mean " << r.minimum_mean << endl;
-    abort();
+    ah_fatal_error("witness mean does not match reported minimum mean");

As per coding guidelines: "Use ah_fatal_error() macro for unrecoverable fatal errors."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Examples/min_mean_cycle_example.cc` around lines 120 - 122, Replace the bare
abort() call with the ah_fatal_error() macro so the fatal condition is reported
through Aleph's error infrastructure; specifically, in the block that prints the
sanity check using witness_mean and r.minimum_mean (the cerr << ... lines and
abort()), call ah_fatal_error() with a descriptive message including
witness_mean and r.minimum_mean instead of abort() so the error is handled by
the project’s logging/diagnostics.
Tests/k2tree_test.cc (1)

44-46: Replace std::vector and std::set with Aleph equivalents throughout

std::vector<Point> (Lines 174, 582, 613, 736) and std::set<std::pair<double,double>> (Lines 662, 935, 943) violate the mandatory container policy. Replace with DynList<Point> and DynSetTree<std::pair<double,double>> respectively, and drop the <vector> / <set> includes in favour of the corresponding Aleph headers.

As per coding guidelines: "MUST use Aleph containers when an equivalent exists. Use DynList<T> instead of std::list<T> or std::vector<T>. Use DynSetTree<T> instead of std::set<T>."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/k2tree_test.cc` around lines 44 - 46, Replace all uses of
std::vector<Point> with Aleph's DynList<Point> (occurrences around the symbols
in Tests/k2tree_test.cc at the noted spots: lines ~174, ~582, ~613, ~736) and
replace std::set<std::pair<double,double>> with
DynSetTree<std::pair<double,double>> (occurrences around lines ~662, ~935,
~943); remove the `#include` <vector> and `#include` <set> headers and add the
corresponding Aleph container headers (e.g., the DynList and DynSetTree headers
used in our codebase) so the file uses Aleph containers everywhere while leaving
<cmath> intact. Ensure template parameters and any construction/iteration sites
are updated to the DynList/DynSetTree APIs if needed.
Tests/geom_algorithms_test_boolean_3d_serializer_aabb_misc.cc (1)

1261-1274: LGTM — .length() is the correct geometric metric here.

Using .size() on a Segment returns the number of vertices (a constant 2), making the old assertions trivially equivalent to 2 > 2 → always false, which would have caused these tests to silently always fail or assert nothing meaningful. .length() returns the Euclidean length and is exactly what's needed to verify that enlarge_src/enlarge_tgt extend the segment.

🔧 Optional: prefer EXPECT_GT over EXPECT_TRUE(a > b) for richer failure output
-  EXPECT_TRUE(s_copy.length() > s.length());
+  EXPECT_GT(s_copy.length(), s.length());
 
-  EXPECT_TRUE(s_copy2.length() > s.length());
+  EXPECT_GT(s_copy2.length(), s.length());

EXPECT_GT prints both values on failure, which makes diagnosing regressions much easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/geom_algorithms_test_boolean_3d_serializer_aabb_misc.cc` around lines
1261 - 1274, Replace the weak boolean assertions with richer failure output: in
the test SegmentEnlargeDiagonal, keep using Segment::length() to compare lengths
after enlarge_src/enlarge_tgt (on s_copy and s_copy2) but change
EXPECT_TRUE(s_copy.length() > s.length()) and EXPECT_TRUE(s_copy2.length() >
s.length()) to EXPECT_GT(s_copy.length(), s.length()) and
EXPECT_GT(s_copy2.length(), s.length()) respectively so failures print both
values and aid debugging; target the assertions involving s_copy, s_copy2,
Segment::length(), enlarge_src, and enlarge_tgt.
K_Shortest_Paths.H (3)

251-255: Cost_Type template parameter is unused in Path_Snapshot.

The struct body contains only Array<Node*> and Array<Arc*>; Cost_Type is never referenced inside it.

♻️ Proposed simplification
-  template <class GT, typename Cost_Type>
-  struct Path_Snapshot
-  {
-    Array<typename GT::Node *> nodes;
-    Array<typename GT::Arc *> arcs;
-  };
+  template <class GT>
+  struct Path_Snapshot
+  {
+    Array<typename GT::Node *> nodes;
+    Array<typename GT::Arc *> arcs;
+  };

All call sites (make_path_snapshot<GT, Cost_Type>, same_prefix_nodes<GT, Cost_Type>, etc.) would need the Cost_Type argument dropped from the Path_Snapshot template arguments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@K_Shortest_Paths.H` around lines 251 - 255, Path_Snapshot's template
parameter Cost_Type is unused; change the declaration of struct Path_Snapshot to
remove the Cost_Type template parameter (so it only depends on GT) and then
update all call sites that reference Path_Snapshot with two template args (e.g.,
make_path_snapshot<GT, Cost_Type>, same_prefix_nodes<GT, Cost_Type>, and any
other instantiations) to pass only GT (e.g., make_path_snapshot<GT>,
same_prefix_nodes<GT>), adjusting forward declarations and using statements
accordingly to ensure builds still find Path_Snapshot<GT>.

1007-1016: O(k) linear scan for the minimum candidate contradicts the documented O(k log k) complexity.

Both yen_k_shortest_paths (lines 1007–1016) and eppstein_k_shortest_paths (lines 1119–1122) use the same O(k) scan-and-swap-last idiom to extract the cheapest candidate on every iteration. Over k iterations this gives O(k²) extraction, whereas the documented complexity for eppstein_k_shortest_paths is O(E + V log V + k log k) — the latter requires a heap.

The main advantage of Eppstein's algorithm is that it achieves O(m + n log n + k log k) complexity, guaranteeing paths are produced in sorted order by weight. The current linear scan negates this.

Replace candidates with an Aleph min-heap (as required by the coding guidelines) so extraction is O(log k):

♻️ Proposed sketch
-    Array<State> candidates;
-    candidates.reserve(k * 3 + 1);
+    // Use a min-heap keyed on total_cost for O(log k) extraction.
+    // Replace Array + linear scan with an Aleph heap implementation.
     ...
-    size_t best_idx = 0;
-    for (size_t i = 1; i < candidates.size(); ++i)
-      if (candidates[i].total_cost < candidates[best_idx].total_cost)
-        best_idx = i;
-
-    State best = candidates[best_idx];
-    if (best_idx + 1 < candidates.size())
-      candidates[best_idx] = candidates.get_last();
-    const auto removed_candidate = candidates.remove_last();
-    (void) removed_candidate;
+    State best = /* heap.top() */;
+    /* heap.pop(); */

Also fix the documented @complexity for eppstein_k_shortest_paths to reflect the actual O(k²) extraction until this is resolved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@K_Shortest_Paths.H` around lines 1007 - 1016, The current linear scan in
yen_k_shortest_paths and eppstein_k_shortest_paths that finds the best candidate
by iterating over candidates (State vector and indices
best_idx/get_last/remove_last) makes extractions O(k) and yields O(k²) overall;
replace the candidates container with an Aleph min-heap (use the provided heap
API for push and pop/min operations) so you push new State entries into the heap
and extract the minimum via heap_pop (O(log k)) instead of the
scan+swap_last/remove_last pattern, adjust any code that relied on
get_last/remove_last to use heap_pop/heap_top, and update the `@complexity` doc
for eppstein_k_shortest_paths if you cannot complete the heap refactor
immediately.

800-871: contains_path deduplication is O(|container| × path_length) per call, giving O(k² × path_length) total.

Both generate_general_deviation_candidates (lines 862–864) and the Yen inner loop (lines 999–1001) call contains_path which walks the full accepted/candidates arrays and does a node+arc sequence comparison for each entry. For the Eppstein case the candidates array can grow to O(k × degree); for Yen's it is O(k). Either way the total cost grows quadratically with k.

A hash of the arc-pointer sequence (inserted into an OLhashTable or DynSetTree of path signatures) would reduce duplicate detection to O(path_length) amortized:

♻️ Suggested approach
// Hash path identity by XOR-combining arc pointer values.
// Keep a DynSetTree<size_t> seen_hashes alongside the candidates array.
// Before appending, compute hash(candidate.arcs) and check seen_hashes.
// This drops the O(k) membership scan to O(log k) or O(1).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@K_Shortest_Paths.H` around lines 800 - 871, The deduplication via
contains_path in generate_general_deviation_candidates (and the Yen inner loop
that also calls contains_path) is O(k) per candidate; replace it with a
path-signature set: compute a compact signature for a Path_State by hashing its
arc-pointer sequence (e.g., XOR/combine reinterpret_cast<size_t>(arc) across
candidate.arcs and include arc count or another small discriminator), maintain a
DynSetTree<size_t> or OLhashTable<size_t> called seen_hashes alongside
candidates/accepted, check seen_hashes before appending, and insert the
signature when you append a candidate; update both
generate_general_deviation_candidates (where candidates.append(candidate) is
called) and the Yen candidate insertion site to use the signature-set check
instead of contains_path to reduce dedup complexity to amortized O(path_length).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/planarity_gephi_nightly.yml:
- Around line 169-176: The script uses re.search (call site: re.search) but the
top import block (which currently lists json, os, pathlib, shutil, stat,
tarfile, urllib.request, zipfile) is missing the re module, causing a NameError
on Windows runners; fix by adding import re to that import list so re.search is
defined and asset scoring won't crash.
- Around line 230-236: The OS check currently uses elif runner_os == "MacOS"
which misses GitHub's "macOS" casing; update the condition to normalize
runner_os (e.g., compare runner_os.lower() to "macos" or otherwise accept
"macOS") so the macOS branch that adjusts score (the block referencing
runner_os, runner_arch, and lower) executes correctly and macOS assets reach the
intended ≥80 threshold.

In `@Blossom_Weighted.H`:
- Around line 356-403: Add full Doxygen `@param`, `@return`, and `@throws` tags to the
public wrapper blossom_maximum_weight_matching and the functor
Compute_Maximum_Weight_General_Matching (including its constructor and
operator()) — document parameters g, matching, weight, sa, and max_cardinality,
describe the return type Blossom_Weighted_Result<long long>, and list possible
exceptions/errors the functions can raise (e.g., invalid input, allocation or
algorithm failures) so the public API has `@brief`, `@param`, `@return`, and `@throws`
for each exposed entry point.

In `@Examples/blossom_example.cc`:
- Around line 78-84: Replace standard library containers in the Example with
Aleph equivalents: change struct Scenario's members from string slug/title and
vector<Node_Pos> nodes and vector<pair<size_t,size_t>> edges to use Aleph types
(keep std::string only if Aleph has none; otherwise use Aleph string type) and
use Aleph::DynArray<Node_Pos> for nodes and
Aleph::DynArray<Aleph::Pair<size_t,size_t>> or DynArray of a small Aleph tuple
for edges; likewise replace the extracted pairs container and the matched_set
usages with Aleph::DynArray and Aleph::DynSetTree respectively (update code
referencing Scenario::nodes, Scenario::edges, the extracted pairs variable, and
matched_set to the new Aleph types and adjust construction/iteration
accordingly).

In `@Examples/hungarian_example.cc`:
- Around line 144-146: The printed label "Assignments (2 tasks left
unassigned):" is hardcoded and fragile; update the output to compute the
unassigned count from ha.get_assignments() rather than using the literal "2".
Retrieve the vector/collection returned by ha.get_assignments(), compute how
many tasks are unassigned (e.g., by comparing number of tasks to number of
assignments or counting sentinel values in the assignments), and print that
computed count in the cout label before iterating over the assignments; refer to
ha and its get_assignments() result when locating the code to change.
- Around line 102-108: Remove the redundant mat.allocate() call: the DynMatrix
constructor already sets up internal storage via set_dimension() so calling
allocate() on the DynMatrix instance (mat.allocate()) is unnecessary here
because the code immediately writes every entry; delete the allocate()
invocation to avoid redundant pre-reservation while leaving the constructor and
the subsequent loop that fills mat(i, j) unchanged.

In `@Examples/planarity_test_example.cc`:
- Around line 315-330: After creating the ofstream objects (fj, fd, fg, fx) and
writing json/dot/graphml/gexf, check each stream's state (e.g., is_open() and
good()) before printing the "written to" messages for json_path, dot_path,
graphml_path, and gexf_path; if a stream failed to open or has errors, emit an
error message (or skip the success message) instead of unconditionally
announcing the file was written. Target the ofstream variables fj, fd, fg, fx
and the corresponding path variables json_path, dot_path, graphml_path,
gexf_path when adding these guards.

In `@K_Shortest_Paths.H`:
- Around line 354-372: The bounds check on arcs in same_prefix_nodes is
off-by-one and can allow spur_index == a.arcs.size(), enabling an OOB when
callers (e.g. code that does p_snapshot.arcs[spur_index]) access that element;
update the guard in same_prefix_nodes to reject cases where a.arcs.size() <=
spur_index or b.arcs.size() <= spur_index so that arcs[spur_index] is guaranteed
to exist before any access using spur_index.
- Around line 1150-1179: Add comprehensive Doxygen to the Yen_K_Shortest_Paths
template and its operator(): document the template parameters with `@tparam`
entries for GT, Distance, and SA; add `@brief` for the class and for operator();
add `@param` for g, source, target, and k; add `@return` describing the
DynList<K_Shortest_Path_Item<GT, typename Distance::Distance_Type>> result; add
`@throws` for possible exceptions propagated by yen_k_shortest_paths and any
precondition violations; and include `@complexity` and `@note` tags to match the
style and detail used in Eppstein_K_Shortest_Paths::operator(). Ensure the
operator() comment sits immediately above Yen_K_Shortest_Paths::operator() and
references yen_k_shortest_paths<GT, Distance, SA> where appropriate.
- Around line 1090-1097: The current early-return in eppstein_k_shortest_paths
(the source==target block that creates a trivial Path<GT>(g, source), appends an
Item with total_cost 0 and returns result) prevents generation of loopy cycles
for k>1 because generate_general_deviation_candidates exits when
base_path.arcs.is_empty(); either document this limitation in the function's
`@note` as "general (loopy) variant does not enumerate cycles when source==target"
or modify the initialization to seed cycle candidates by iterating the outgoing
arcs from source and pushing those one-step cycle starters into the main
candidate queue before entering the main loop (so
generate_general_deviation_candidates can explore s→a→…→s cycles); update
references to eppstein_k_shortest_paths, generate_general_deviation_candidates,
and the base_path.arcs.is_empty() behavior accordingly.
- Around line 875-897: Update the Doxygen block for yen_k_shortest_paths to
include an `@complexity` clause (matching the same complexity text used in
eppstein_k_shortest_paths) and add a `@note` about thread-safety stating that the
function may mutate the graph internally (it uses const_cast / Cookie_Saver) and
therefore is not thread-safe when the same graph is accessed concurrently;
ensure the wording parallels the eppstein_k_shortest_paths entries for
consistency.
- Around line 150-183: Remove the dead class In_Iterator_Adapter: delete the
entire template<class GT, class SA> class In_Iterator_Adapter definition and its
methods (constructor, has_curr, next_ne, get_current_arc_ne, get_tgt_node) since
it is unused; ensure no other code references In_Iterator_Adapter or its members
(it wraps In_Iterator<GT,SA> and delegates to methods like has_curr(),
next_ne(), get_curr_ne(), get_tgt_node()), and run a build to confirm no
remaining dependencies.
- Around line 473-490: The equality check cand == dist_node (and the analogous
cand == dist_u in the digraph code) is fragile for floating-point Cost_Type;
change the fallback arc-selection to use an epsilon-based comparison or track
the best-cost candidate instead: iterate the Node_Arc_Iterator loop (referencing
selected, index.dist_to_target, Node_Arc_Iterator<GT, SA>, get_current_arc_ne,
shortest_path_detail::checked_add, distance(arc), dist_node) and either (a)
treat cand as matching if fabs(cand - dist_node) <= eps where eps is derived
from Cost_Type (or relative tolerance), or (b) keep the arc with the minimal
|cand - dist_node| and select it after the loop; apply the same change to the
digraph counterpart that uses cand == dist_u so no correct tree arc is missed or
a wrong arc chosen.

---

Outside diff comments:
In `@Tests/geom_algorithms_test_boolean_3d_serializer_aabb_misc.cc`:
- Around line 1604-1654: The two tests
ConvexPolygonOffsetCollinearConsecutiveVertices and
ConvexPolygonOffsetLargeInwardOffset assert correct behavior but are known to
fail; either disable them by renaming the test cases to prefix DISABLED_ (e.g.,
DISABLED_ConvexPolygonOffsetCollinearConsecutiveVertices,
DISABLED_ConvexPolygonOffsetLargeInwardOffset) so CI skips them, or convert them
into regression-guard checks that assert the current buggy behavior (e.g.,
replace EXPECT_LT(offset_area, orig_area) with EXPECT_GT(offset_area, orig_area)
in ConvexPolygonOffsetCollinearConsecutiveVertices and replace
EXPECT_LT(result.size(), 3u) with EXPECT_GE(result.size(), 3u) in
ConvexPolygonOffsetLargeInwardOffset) and add comments referencing the bug id;
pick one approach consistently and keep test names (or comments) indicating that
these are temporary until ConvexPolygonOffset is fixed.

In `@Tests/k2tree_test.cc`:
- Line 462: The EXPECT_GE(result.size(), 0) assertion is trivially true because
result.size() is unsigned; replace or remove it: either compute an
expected_count via a brute-force scan of the inserted points and assert
EXPECT_EQ(result.size(), expected_count) (or EXPECT_GE/EXPECT_LE with known
bounds), or remove the assertion and add a comment that the surrounding loop is
a crash-safety smoke test; locate and update the EXPECT_GE(result.size(), 0)
line and the surrounding test logic that produces result to implement the
meaningful comparison.

---

Duplicate comments:
In `@Dominators.H`:
- Around line 276-280: The method do_compute currently takes a const GT& but
uses const_cast to assign gptr and then mutates graph internals (gptr, root_ptr,
is_computed); change do_compute signature to take GT& (non-const), remove the
const_cast in the body and assign gptr = &g, keep root_ptr and is_computed
logic, and propagate the non-const GT& signature to all callers (update call
sites and any upstream declarations) so no const is cast away anywhere.
- Around line 304-315: The function returns early when num_reachable <= 1 before
allocating pred, but later code (dominance_frontiers) accesses pred[0], causing
out-of-bounds for single-node graphs; fix by allocating pred =
Array<DynList<long>>(static_cast<size_t>(num_reachable), DynList<long>()) before
the early-return (or alternatively add a guard that skips dominance_frontiers
when num_reachable <= 1), and ensure idom_arr[0] and is_computed remain set as
they are for the single-node case so subsequent callers see a valid, consistent
state.

In `@Examples/dominators_example.cc`:
- Around line 65-83: Update the ASCII control-flow diagram in
Examples/dominators_example.cc so the edges from then to join and else to join
are explicitly drawn (e.g., add arrows or connector lines showing "then → join"
and "else → join") rather than relying on implicit layout; locate the diagram
block containing the labels entry, cond, then, else, join, loop_header,
loop_body, exit and modify the lines around then and else to clearly show their
connections into join.

In `@Examples/min_mean_cycle_example.cc`:
- Around line 112-123: Sanity check for minimum mean cycle has been added by
computing witness_mean from r.cycle_total_cost and r.cycle_length and comparing
it to r.minimum_mean; no change required — keep the block that computes
witness_mean, the std::abs comparison against static_cast<long
double>(r.minimum_mean), and the diagnostic cerr followed by abort() as-is (look
for witness_mean, r.cycle_total_cost, r.cycle_length, r.minimum_mean, cerr,
abort).

In `@Examples/planarity_test_example.cc`:
- Around line 62-78: Move the Doxygen block so it precedes the template
declaration rather than sitting between `template <class GT>` and the function
signature; specifically, relocate the comment that documents `print_result` to
immediately before the `template <class GT>` line for `print_result` (and do the
same for the Doxygen comment for `print_drawing`) so Doxygen associates the
documentation with the templated functions.

In `@Tests/ah-functional.cc`:
- Around line 1537-1545: The Doxygen comment and the FloatToleranceEqual
constructor are placed at column 0 rather than as indented members of the
struct; move the entire /** ... */ block and the constructor definition
FloatToleranceEqual(float tol = 0.001f) : tolerance(tol) {} so they are properly
indented inside the struct body (aligned with other member declarations) so
Doxygen associates the comment with the constructor/member `tolerance`.

In `@Tests/blossom_test.cc`:
- Around line 110-120: The helper template build_graph currently takes a
std::vector for edges and other tests use std::vector/STD buffers; replace those
with Aleph containers (e.g., DynArray<std::pair<size_t,size_t>> or Array where
fixed-size appropriate). Change the build_graph signature and callers to
accept/produce an Aleph DynArray instead of std::vector, populate aleph_edges
using push_back or proper DynArray resizing/assignment (avoid direct indexing on
un-sized container), and update the other helpers/DP buffers referenced around
the 158-197 ranges to use DynArray/Array equivalents so no std::vector remains.
Ensure types (pair<size_t,size_t>) match and preserve arc_info behavior when
forwarding to build_graph<GT>.

---

Nitpick comments:
In `@Examples/hungarian_example.cc`:
- Around line 137-141: The local variable ha (constructed as
Hungarian_Assignment<int> ha({...})) is not modified after construction—change
its declaration to const Hungarian_Assignment<int> ha({...}) to match the
pattern used in example_basic_assignment and to signal immutability; locate the
Hungarian_Assignment<int> ha construction and prefix it with const.
- Around line 104-107: Replace the C-style array 'data' with a std::array to
follow guidelines: change the declaration of data to std::array<std::array<int,
3>, 3> data = {{{{10,5,13}},{{3,9,18}},{{10,6,12}}}}; (or equivalent nested
std::array initializer), add `#include` <array> if missing, and keep the existing
loops that assign mat(i, j) = data[i][j]; so only the type of 'data' and its
initializer change while preserving its indexing and use in the mat population
code.
- Around line 57-80: The cost matrix is duplicated as a literal and a C-style
int[4][4]; define a single constexpr std::array<std::array<int,4>,4> (e.g.
constexpr std::array<std::array<int,4>,4> cost = {{ {...}, {...}, {...}, {...}
}}), use that same constexpr when constructing Hungarian_Assignment<int> (pass
cost) and use cost[r][c] for the detailed output; replace the C-style costs
array with this std::array and update references to ha, Hungarian_Assignment,
and costs accordingly so there is a single compile-time source of truth.

In `@Examples/min_mean_cycle_example.cc`:
- Line 144: Replace the explicit pointer declaration "Arc * arc =
it.get_curr();" with type-deducing auto to follow project C++20 style: use "auto
* arc = it.get_curr();" (i.e., change the declaration in
Examples/min_mean_cycle_example.cc where Arc, arc and it.get_curr() are used) so
the compiler deduces the pointer type from the iterator's get_curr() return
type.
- Around line 156-168: The anonymous namespace closing brace '}' is fused with
the Doxygen comment for main (the line starting with "} /**"), which is
confusing; edit the closing brace for the anonymous namespace (the lone '}') so
it sits on its own line followed by a blank line, then keep the Doxygen block
(/** ... `@brief` ... */) for main() starting on the next line; this affects the
anonymous namespace closing token and the main() Doxygen comment near the end of
Examples/min_mean_cycle_example.cc.
- Around line 120-122: Replace the bare abort() call with the ah_fatal_error()
macro so the fatal condition is reported through Aleph's error infrastructure;
specifically, in the block that prints the sanity check using witness_mean and
r.minimum_mean (the cerr << ... lines and abort()), call ah_fatal_error() with a
descriptive message including witness_mean and r.minimum_mean instead of abort()
so the error is handled by the project’s logging/diagnostics.

In `@Examples/planarity_test_example.cc`:
- Around line 180-448: main is long because it contains four large inline demo
blocks; extract each block into its own helper function (e.g.,
demo_planar_sample(UGraph&, const Planarity_Test_Options&, const fs::path&),
demo_dense_sample(...), demo_k33_certificate(const fs::path&, const
Planarity_Test_Options&), demo_digraph_sample(DGraph&, const
Planarity_Test_Options&)) and move the corresponding logic (graph construction,
planarity_test calls, printing, certificate file writing and validation) into
those functions, preserving use of symbols like Planarity_Test_Options,
strict_lr_opts, planarity_test, nonplanar_certificate_to_json/dot/graphml/gexf,
validate_nonplanar_certificate, planar_geometric_drawing, planar_dual_metadata;
make each helper take only the inputs it needs (output_dir and opts where
appropriate), return void (or a small result struct if needed), and replace the
inline blocks in main with simple calls to these helpers to improve readability.

In `@K_Shortest_Paths.H`:
- Around line 251-255: Path_Snapshot's template parameter Cost_Type is unused;
change the declaration of struct Path_Snapshot to remove the Cost_Type template
parameter (so it only depends on GT) and then update all call sites that
reference Path_Snapshot with two template args (e.g., make_path_snapshot<GT,
Cost_Type>, same_prefix_nodes<GT, Cost_Type>, and any other instantiations) to
pass only GT (e.g., make_path_snapshot<GT>, same_prefix_nodes<GT>), adjusting
forward declarations and using statements accordingly to ensure builds still
find Path_Snapshot<GT>.
- Around line 1007-1016: The current linear scan in yen_k_shortest_paths and
eppstein_k_shortest_paths that finds the best candidate by iterating over
candidates (State vector and indices best_idx/get_last/remove_last) makes
extractions O(k) and yields O(k²) overall; replace the candidates container with
an Aleph min-heap (use the provided heap API for push and pop/min operations) so
you push new State entries into the heap and extract the minimum via heap_pop
(O(log k)) instead of the scan+swap_last/remove_last pattern, adjust any code
that relied on get_last/remove_last to use heap_pop/heap_top, and update the
`@complexity` doc for eppstein_k_shortest_paths if you cannot complete the heap
refactor immediately.
- Around line 800-871: The deduplication via contains_path in
generate_general_deviation_candidates (and the Yen inner loop that also calls
contains_path) is O(k) per candidate; replace it with a path-signature set:
compute a compact signature for a Path_State by hashing its arc-pointer sequence
(e.g., XOR/combine reinterpret_cast<size_t>(arc) across candidate.arcs and
include arc count or another small discriminator), maintain a DynSetTree<size_t>
or OLhashTable<size_t> called seen_hashes alongside candidates/accepted, check
seen_hashes before appending, and insert the signature when you append a
candidate; update both generate_general_deviation_candidates (where
candidates.append(candidate) is called) and the Yen candidate insertion site to
use the signature-set check instead of contains_path to reduce dedup complexity
to amortized O(path_length).

In `@Tests/geom_algorithms_test_boolean_3d_serializer_aabb_misc.cc`:
- Around line 1261-1274: Replace the weak boolean assertions with richer failure
output: in the test SegmentEnlargeDiagonal, keep using Segment::length() to
compare lengths after enlarge_src/enlarge_tgt (on s_copy and s_copy2) but change
EXPECT_TRUE(s_copy.length() > s.length()) and EXPECT_TRUE(s_copy2.length() >
s.length()) to EXPECT_GT(s_copy.length(), s.length()) and
EXPECT_GT(s_copy2.length(), s.length()) respectively so failures print both
values and aid debugging; target the assertions involving s_copy, s_copy2,
Segment::length(), enlarge_src, and enlarge_tgt.

In `@Tests/k2tree_test.cc`:
- Around line 44-46: Replace all uses of std::vector<Point> with Aleph's
DynList<Point> (occurrences around the symbols in Tests/k2tree_test.cc at the
noted spots: lines ~174, ~582, ~613, ~736) and replace
std::set<std::pair<double,double>> with DynSetTree<std::pair<double,double>>
(occurrences around lines ~662, ~935, ~943); remove the `#include` <vector> and
`#include` <set> headers and add the corresponding Aleph container headers (e.g.,
the DynList and DynSetTree headers used in our codebase) so the file uses Aleph
containers everywhere while leaving <cmath> intact. Ensure template parameters
and any construction/iteration sites are updated to the DynList/DynSetTree APIs
if needed.

Refactor common test helpers for graph construction and functional algorithms into a dedicated header, `test_graph_helpers.h`. This reduces code duplication across test files for LCA, HLD, and Blossom algorithms. Additionally, update comments in `ahFunctional.H` and `Blossom_Weighted.H` for clarity, and refine input validation in `Min_Cost_Matching.H` and `Hungarian.H` to use `__int128` for intermediate cost calculations, preventing overflow with `LLONG_MIN`. Update Planarity example and Blossom example outputs to use `std::cout` and `std::string` formatting instead of `printf`.
refactor: Convert nightly Gephi comparison to weekly.
This change involves renaming related files and update corresponding scripts to reflect a weekly execution schedule for Gephi comparison and reporting.

test: Refactor cut nodes and blossom tests

test: Improve graph construction and assertions in cut nodes and blossom tests.
The test utilities for building graphs and verifying matching or cut node properties have been refactored for clarity and robustness, including changes to node insertion logic and assertion helpers.

docs: Update README for weekly Gephi comparison

docs: Update README to reflect weekly Gephi comparison script.
The README has been updated to reference the new 'weekly' comparison script instead of the 'nightly' one.
Copilot AI review requested due to automatic review settings February 23, 2026 04:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 73 changed files in this pull request and generated 2 comments.

Modernizes several C++ example files to use C++11/17 features like std::array and std::format, improving readability and consistency. Also introduces `test_graph_helpers.h` to centralize graph building utilities for tests.
@lrleon lrleon self-assigned this Feb 23, 2026
docs: Introduce GitHub Pages documentation workflow
refactor(K_Shortest_Paths): Reformat Doxygen comments
chore: Update .gitignore with new patterns and generated files
Copilot AI review requested due to automatic review settings February 23, 2026 16:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 78 changed files in this pull request and generated no new comments.

Introduces Ruby scripts for Gephi nightly artifact comparison, including render profiles and templates. Also adds a K33 non-planar graphml fixture for testing. Refactors dominators_example.cc for clarity by moving print_frontier to a local namespace.
Refactor run_external_certificate_validator to use a new run_script_cmd
function, which sets GITHUB_OUTPUT to empty to prevent scripts from writing
to the CI output file. This improves test reliability for external script
invocations.
Copilot AI review requested due to automatic review settings February 23, 2026 19:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 82 changed files in this pull request and generated 5 comments.

Comment on lines +224 to +247
DynSetTree<std::pair<size_t, size_t>> matched_set(output.matched_pairs);

std::ofstream out(file_path);
if (not out)
{
std::cerr << "Cannot write file: " << file_path << "\n";
return;
}

out << "\\documentclass[tikz,border=10pt]{standalone}\n";
out << "\\usepackage{tikz}\n";
out << "\\usetikzlibrary{arrows.meta,calc}\n";
out << "\\begin{document}\n";
out << "\\begin{tikzpicture}[x=1cm,y=1cm]\n";

out << " \\path[shade, left color=cyan!6, right color=blue!5] "
<< "(-4.6,-3.4) rectangle (4.6,3.4);\n";

out << " \\node[font=\\bfseries\\large] at (0,3.05) {"
<< s.title << "};\n";

out << " \\node[font=\\small] at (0,2.65) {objective: "
<< (max_cardinality ? "max-cardinality then max-weight" : "pure max-weight")
<< "};\n";
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DynSetTree doesn’t appear to use a contains() API elsewhere in this PR (tests use exist() / search()); this will likely fail to compile. Consider switching to the set’s supported membership check (e.g., matched_set.exist(std::make_pair(a, b)) or matched_set.search(...) != nullptr) and, if DynSetTree doesn’t support constructing from output.matched_pairs, populate matched_set via inserts in a loop.

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +278
for (const auto & [u_raw, v_raw, w] : s.edges)
{
size_t u = u_raw;
size_t v = v_raw;
size_t a = std::min(u, v);
size_t b = std::max(u, v);
const bool matched = matched_set.contains(std::make_pair(a, b));

out << " \\draw[" << (matched ? "match" : "edge") << "] (n"
<< u << ") -- (n" << v << ");\n";

out << " \\node[wlabel] at ($(n" << u << ")!0.5!(n" << v << ")$) {"
<< w << "};\n";
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DynSetTree doesn’t appear to use a contains() API elsewhere in this PR (tests use exist() / search()); this will likely fail to compile. Consider switching to the set’s supported membership check (e.g., matched_set.exist(std::make_pair(a, b)) or matched_set.search(...) != nullptr) and, if DynSetTree doesn’t support constructing from output.matched_pairs, populate matched_set via inserts in a loop.

Copilot uses AI. Check for mistakes.
cout << " (no path found)\n";
cout << '\n';
}
} /**
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closing brace for the anonymous namespace and the following Doxygen comment are on the same line (} /**), which harms readability and can confuse documentation tooling. Put the } on its own line, add a blank line, and then start the comment block.

Suggested change
} /**
}
/**

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +174
* - full result with witness cycle,
* - value-only API reporting only the minimum mean,
* - filtered run that blocks a specified arc (D→E) via the Arc_Visible predicate.
* Results are printed to standard output.
*
* @return int Exit status (0 on success).
*/
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list items in the Doxygen comment are misaligned (extra leading space before * on lines 168–171). Align the leading * consistently so Doxygen renders the bullet list correctly.

Suggested change
* - full result with witness cycle,
* - value-only API reporting only the minimum mean,
* - filtered run that blocks a specified arc (D→E) via the Arc_Visible predicate.
* Results are printed to standard output.
*
* @return int Exit status (0 on success).
*/
* - full result with witness cycle,
* - value-only API reporting only the minimum mean,
* - filtered run that blocks a specified arc (D→E) via the Arc_Visible predicate.
* Results are printed to standard output.
*
* @return int Exit status (0 on success).
*/

Copilot uses AI. Check for mistakes.
Update planarity_test.cc to include more detailed diagnostics and improve readability.
Adjust planarity_test_example.cc to enhance the summary output and clarify comments.
Copilot AI review requested due to automatic review settings February 23, 2026 20:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 82 changed files in this pull request and generated no new comments.

Refactor code in Dominators.H and HLD.H for clarity and better handling of node mappings and subtree size calculations.
Copilot AI review requested due to automatic review settings February 23, 2026 20:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 82 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants