diff --git a/README.md b/README.md
index c5ac828..1763a67 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,14 @@
+
+ |
+
+
# graph-v3
**A modern C++20 graph library — header-only, descriptor-based, works with your containers.**
+ |
+
+
[](https://en.cppreference.com/w/cpp/20)
@@ -17,7 +24,7 @@
- **13 algorithms** — Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard coefficient
- **7 lazy views** — vertexlist, edgelist, incidence, neighbors, BFS, DFS, topological sort — all composable with range adaptors
- **Customization Point Objects (CPOs)** — adapt existing data structures without modifying them
-- **3 containers, 26 trait combinations** — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list` with mix-and-match vertex/edge storage
+- **3 containers, 27 trait combinations** — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list` with mix-and-match vertex/edge storage
- **4261 tests passing** — comprehensive Catch2 test suite
---
@@ -78,7 +85,7 @@ Both share a common descriptor system and customization-point interface.
|----------|-----------------|---------|
| **Algorithms** | Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard | [Algorithm reference](docs/status/implementation_matrix.md#algorithms) |
| **Views** | vertexlist, edgelist, incidence, neighbors, BFS, DFS, topological sort | [View reference](docs/status/implementation_matrix.md#views) |
-| **Containers** | `dynamic_graph` (26 trait combos), `compressed_graph` (CSR), `undirected_adjacency_list` | [Container reference](docs/status/implementation_matrix.md#containers) |
+| **Containers** | `dynamic_graph` (27 trait combos), `compressed_graph` (CSR), `undirected_adjacency_list` | [Container reference](docs/status/implementation_matrix.md#containers) |
| **CPOs** | 19 customization point objects (vertices, edges, target_id, vertex_value, edge_value, …) | [CPO reference](docs/reference/cpo-reference.md) |
| **Concepts** | 9 graph concepts (edge, vertex, adjacency_list, …) | [Concepts reference](docs/reference/concepts.md) |
@@ -174,4 +181,4 @@ Distributed under the [Boost Software License 1.0](LICENSE).
---
-**Status:** 4261 / 4261 tests passing · 13 algorithms · 7 views · 3 containers · 26 trait combinations · C++20 · BSL-1.0
+**Status:** 4261 / 4261 tests passing · 13 algorithms · 7 views · 3 containers · 27 trait combinations · C++20 · BSL-1.0
diff --git a/agents/archive/dynamic_graph_todo.md b/agents/archive/dynamic_graph_todo.md
index af5b227..d7cb4a1 100644
--- a/agents/archive/dynamic_graph_todo.md
+++ b/agents/archive/dynamic_graph_todo.md
@@ -20,8 +20,8 @@
- ✅ uous (unordered_map + unordered_set): Basic + CPO tests COMPLETE (53 test cases, 563 assertions)
**Phase 4.3: Map-Based Edge Containers - COMPLETE ✅**
-- ✅ voem (vector + map edges): Basic + CPO tests COMPLETE (46 test cases, 292 assertions)
-- ✅ moem (map + map edges): Basic + CPO tests COMPLETE (53 test cases, 578 assertions)
+- ✅ vom (vector + map edges): Basic + CPO tests COMPLETE (46 test cases, 292 assertions)
+- ✅ mom (map + map edges): Basic + CPO tests COMPLETE (53 test cases, 578 assertions)
**Phase 4 Overall: 100% COMPLETE ✅ (10/10 traits implemented)**
@@ -102,8 +102,8 @@
- test_dynamic_graph_uous.cpp + test_dynamic_graph_cpo_uous.cpp ✅
*Map Edge Containers (4 files):*
-- test_dynamic_graph_voem.cpp + test_dynamic_graph_cpo_voem.cpp ✅
-- test_dynamic_graph_moem.cpp + test_dynamic_graph_cpo_moem.cpp ✅
+- test_dynamic_graph_vom.cpp + test_dynamic_graph_cpo_vom.cpp ✅
+- test_dynamic_graph_mom.cpp + test_dynamic_graph_cpo_mom.cpp ✅
*Additional Test Files:*
- test_dynamic_graph_common.cpp ✅
@@ -965,42 +965,42 @@ All prerequisites for both std::set (Phase 4.1) and std::unordered_set (Phase 4.
| 4.3.1b | Identify changes needed for map-based edge access (key vs iterator) | ✅ DONE |
| 4.3.1c | Design edge_descriptor changes for map-based edges (if any) | ✅ DONE |
-**Step 4.3.2: Create voem_graph_traits (vector + edge map)** ✅ **COMPLETE** (2024-12-28)
+**Step 4.3.2: Create vom_graph_traits (vector + edge map)** ✅ **COMPLETE** (2024-12-28)
**Implementation Summary:**
- Added is_map_based_edge_container concept to container_utility.hpp
- Added emplace_edge helper for pair-wrapped edge insertion
- Updated edge_descriptor::target_id() to unwrap map pairs
- Updated edge_value CPO to extract values from map pairs
-- Created voem_graph_traits.hpp with std::map edges
-- Created test_dynamic_graph_voem.cpp (~741 lines, 25 test cases)
-- Created test_dynamic_graph_cpo_voem.cpp (~1273 lines, 21 test cases)
+- Created vom_graph_traits.hpp with std::map edges
+- Created test_dynamic_graph_vom.cpp (~741 lines, 25 test cases)
+- Created test_dynamic_graph_cpo_vom.cpp (~1273 lines, 21 test cases)
- All 46 test cases passing
| Step | Task | Status |
|------|------|--------|
-| 4.3.2a | Create voem_graph_traits.hpp | ✅ DONE |
+| 4.3.2a | Create vom_graph_traits.hpp | ✅ DONE |
| 4.3.2b | Update load_edges or edge insertion for map semantics | ✅ DONE |
-| 4.3.2c | Create test_dynamic_graph_voem.cpp basic tests (~800 lines) | ✅ DONE |
-| 4.3.2d | Create test_dynamic_graph_cpo_voem.cpp CPO tests (~1200 lines) | ✅ DONE |
+| 4.3.2c | Create test_dynamic_graph_vom.cpp basic tests (~800 lines) | ✅ DONE |
+| 4.3.2d | Create test_dynamic_graph_cpo_vom.cpp CPO tests (~1200 lines) | ✅ DONE |
| 4.3.2e | Update CMakeLists.txt and verify tests pass | ✅ DONE |
-**Step 4.3.3: Create moem_graph_traits (map vertices + edge map)** ✅ **COMPLETE** (2024-12-28)
+**Step 4.3.3: Create mom_graph_traits (map vertices + edge map)** ✅ **COMPLETE** (2024-12-28)
**Implementation Summary:**
-- Created moem_graph_traits.hpp with std::map vertices and std::map edges
+- Created mom_graph_traits.hpp with std::map vertices and std::map edges
- Simplified operator[] using at() for both container types (throws if not found)
- Added is_map_based_vertex_container concept
- Tests derived from mos (which also has map vertices)
-- Created test_dynamic_graph_moem.cpp (~1123 lines, 27 test cases)
-- Created test_dynamic_graph_cpo_moem.cpp (~1274 lines, 26 test cases)
+- Created test_dynamic_graph_mom.cpp (~1123 lines, 27 test cases)
+- Created test_dynamic_graph_cpo_mom.cpp (~1274 lines, 26 test cases)
- All 53 test cases passing
| Step | Task | Status |
|------|------|--------|
-| 4.3.3a | Create moem_graph_traits.hpp | ✅ DONE |
-| 4.3.3b | Create test_dynamic_graph_moem.cpp (~800 lines) | ✅ DONE |
-| 4.3.3c | Create test_dynamic_graph_cpo_moem.cpp (~1200 lines) | ✅ DONE |
+| 4.3.3a | Create mom_graph_traits.hpp | ✅ DONE |
+| 4.3.3b | Create test_dynamic_graph_mom.cpp (~800 lines) | ✅ DONE |
+| 4.3.3c | Create test_dynamic_graph_cpo_mom.cpp (~1200 lines) | ✅ DONE |
| 4.3.3d | Update CMakeLists.txt and verify tests pass | ✅ DONE |
---
@@ -1010,7 +1010,7 @@ All prerequisites for both std::set (Phase 4.1) and std::unordered_set (Phase 4.
**Total New Traits:**
- Set edges: vos, dos, mos, uos (4 traits)
- Unordered set edges: vous, dous, mous, uous (4 traits)
-- Map edges: voem, moem (2 traits)
+- Map edges: vom, mom (2 traits)
**Implementation Changes:**
- operator<=> for dynamic_edge (generates <, >, <=, >=)
@@ -1038,8 +1038,8 @@ that map/unordered_map-based vertex containers work correctly with various ID ty
the standard integral types.
**Current State Analysis:**
-- Map-based containers (mos, moem, mol, etc.) already support `std::string` vertex IDs
-- Tests exist with `std::string` IDs in: mos, mous, moem, mol, mov, mod, mofl tests
+- Map-based containers (mos, mom, mol, etc.) already support `std::string` vertex IDs
+- Tests exist with `std::string` IDs in: mos, mous, mom, mol, mov, mod, mofl tests
- Basic string ID functionality is validated but not comprehensively tested
- No tests exist for: double IDs, compound types, custom types with complex comparison
@@ -1088,7 +1088,7 @@ the standard integral types.
|-------|-------------|-----------|-----------------|
| mos | 3 test cases | 22 sections | ✅ Comprehensive |
| mous | 3 test cases | 22 sections | ✅ Comprehensive |
-| moem | 3 test cases | 22 sections | ✅ Comprehensive |
+| mom | 3 test cases | 22 sections | ✅ Comprehensive |
| mol | 3 test cases | ~20 sections | ✅ Comprehensive |
| mov | 3 test cases | ~20 sections | ✅ Comprehensive |
| mod | 3 test cases | ~20 sections | ✅ Comprehensive |
@@ -2080,7 +2080,7 @@ For Phase 5 (non-integral):
1. **Finish Phase 4.1.4** - Complete mos CPO tests (1-2 days)
2. **Phase 4.1.5** - uos (unordered_map + set) basic + CPO tests (2-3 days)
3. **Phase 4.2** - Unordered set edge containers (vous, dous, mous, uous) (4-5 days)
-4. **Phase 4.3** - Map-based edge containers (voem, moem) (3-4 days)
+4. **Phase 4.3** - Map-based edge containers (vom, mom) (3-4 days)
5. **Phase 7.3** - Edge cases (ongoing, 2-3 days)
6. **Phase 6** - Integration tests (2-3 days)
7. **Phase 7.1** - Mutation operations (2-3 days)
diff --git a/agents/archive/test_reorganization_execution_plan.md b/agents/archive/test_reorganization_execution_plan.md
index c567000..9569e0d 100644
--- a/agents/archive/test_reorganization_execution_plan.md
+++ b/agents/archive/test_reorganization_execution_plan.md
@@ -149,8 +149,8 @@ git mv tests/test_dynamic_graph_uol.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_uov.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_uod.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_vos.cpp tests/container/dynamic_graph/
-git mv tests/test_dynamic_graph_voem.cpp tests/container/dynamic_graph/
-git mv tests/test_dynamic_graph_moem.cpp tests/container/dynamic_graph/
+git mv tests/test_dynamic_graph_vom.cpp tests/container/dynamic_graph/
+git mv tests/test_dynamic_graph_mom.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_dos.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_mos.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_uos.cpp tests/container/dynamic_graph/
@@ -190,8 +190,8 @@ git mv tests/test_dynamic_graph_cpo_uol.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_cpo_uov.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_cpo_uod.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_cpo_vos.cpp tests/container/dynamic_graph/
-git mv tests/test_dynamic_graph_cpo_voem.cpp tests/container/dynamic_graph/
-git mv tests/test_dynamic_graph_cpo_moem.cpp tests/container/dynamic_graph/
+git mv tests/test_dynamic_graph_cpo_vom.cpp tests/container/dynamic_graph/
+git mv tests/test_dynamic_graph_cpo_mom.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_cpo_dos.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_cpo_mos.cpp tests/container/dynamic_graph/
git mv tests/test_dynamic_graph_cpo_uos.cpp tests/container/dynamic_graph/
@@ -309,8 +309,8 @@ add_executable(graph3_container_tests
dynamic_graph/test_dynamic_graph_uov.cpp
dynamic_graph/test_dynamic_graph_uod.cpp
dynamic_graph/test_dynamic_graph_vos.cpp
- dynamic_graph/test_dynamic_graph_voem.cpp
- dynamic_graph/test_dynamic_graph_moem.cpp
+ dynamic_graph/test_dynamic_graph_vom.cpp
+ dynamic_graph/test_dynamic_graph_mom.cpp
dynamic_graph/test_dynamic_graph_dos.cpp
dynamic_graph/test_dynamic_graph_mos.cpp
dynamic_graph/test_dynamic_graph_uos.cpp
@@ -350,8 +350,8 @@ add_executable(graph3_container_tests
dynamic_graph/test_dynamic_graph_cpo_uov.cpp
dynamic_graph/test_dynamic_graph_cpo_uod.cpp
dynamic_graph/test_dynamic_graph_cpo_vos.cpp
- dynamic_graph/test_dynamic_graph_cpo_voem.cpp
- dynamic_graph/test_dynamic_graph_cpo_moem.cpp
+ dynamic_graph/test_dynamic_graph_cpo_vom.cpp
+ dynamic_graph/test_dynamic_graph_cpo_mom.cpp
dynamic_graph/test_dynamic_graph_cpo_dos.cpp
dynamic_graph/test_dynamic_graph_cpo_mos.cpp
dynamic_graph/test_dynamic_graph_cpo_uos.cpp
diff --git a/agents/archive/view_plan.md b/agents/archive/view_plan.md
index ed155ba..727bbad 100644
--- a/agents/archive/view_plan.md
+++ b/agents/archive/view_plan.md
@@ -739,7 +739,7 @@ Created `tests/views/test_edgelist.cpp`:
- Value function receives descriptor
- Structured bindings work: `for (auto [e] : edgelist(g))` and `for (auto [e, val] : edgelist(g, evf))`
- Tests pass with sanitizers
-- Map-based containers supported (vov, voem, mov, moem)
+- Map-based containers supported (vov, vom, mov, mom)
**Commit Message**:
```
diff --git a/agents/archive/view_strategy.md b/agents/archive/view_strategy.md
index 216389c..7c80212 100644
--- a/agents/archive/view_strategy.md
+++ b/agents/archive/view_strategy.md
@@ -666,11 +666,11 @@ with both index-based and iterator-based vertex/edge storage:
**Edge Container Types**:
- `vector` - Random-access edges
-- `map` (voem, moem, etc.) - Sorted edges by target, deduplicated
+- `map` (vom, mom, etc.) - Sorted edges by target, deduplicated
- `list` - Forward-only edges
**Test Matrix** (minimum coverage per view):
-| View | vov | mov | voem | moem |
+| View | vov | mov | vom | mom |
|------|-----|-----|------|------|
| vertexlist | ✓ | ✓ | ✓ | ✓ |
| incidence | ✓ | ✓ | ✓ | ✓ |
diff --git a/agents/bgl2_comparison_result.md b/agents/bgl2_comparison_result.md
index 416e1ec..9caed7a 100644
--- a/agents/bgl2_comparison_result.md
+++ b/agents/bgl2_comparison_result.md
@@ -435,9 +435,9 @@ using G = dynamic_graph` | Traits-configured vertex and edge containers | Mutable | General purpose, flexible container choice |
| `compressed_graph` | CSR (compressed sparse row) | Immutable after construction | Read-only, high-performance, memory-compact |
-| `undirected_adjacency_list` | Dual doubly-linked lists per edge | Mutable, O(1) edge removal | Undirected graphs, frequent edge insertion/removal |
+| `undirected_adjacency_list` | Dual doubly-linked lists per edge | Mutable, O(1) edge removal | Undirected graphs, frequent edge insertion/removal |
**Full `dynamic_graph` container matrix (26 combinations):**
@@ -278,7 +278,7 @@ Edge containers:
Traits naming convention: `{vertex}o{edge}_graph_traits` (e.g., `vov_graph_traits` = vector vertices, vector edges).
-All 26 combinations listed with trait file names (vov, vod, vofl, vol, vos, vous, voem, dov, dod, dofl, dol, dos, dous, mov, mod, mofl, mol, mos, mous, moem, uov, uod, uofl, uol, uos, uous).
+All 27 combinations listed with trait file names (vov, vod, vofl, vol, vos, vous, vom, voum, dov, dod, dofl, dol, dos, dous, mov, mod, mofl, mol, mos, mous, mom, uov, uod, uofl, uol, uos, uous).
### Phase 4: Reference
diff --git a/agents/incoming_edges_design.md b/agents/incoming_edges_design.md
new file mode 100644
index 0000000..f32acf9
--- /dev/null
+++ b/agents/incoming_edges_design.md
@@ -0,0 +1,833 @@
+# Incoming Edge Support — Design Document
+
+## 1. Problem Statement
+
+The graph-v3 library currently provides **outgoing edge access only**. All edge CPOs
+(`edges()`, `degree()`, `target_id()`), views (`incidence`, `edgelist`, `neighbors`),
+concepts (`adjacency_list`, `vertex_edge_range`), and algorithms are implicitly outgoing.
+There is no way to query "which edges point **to** a vertex."
+
+This document proposes extending the library to support **incoming edges** while
+preserving full backward compatibility with existing code.
+
+---
+
+## 2. Design Principles
+
+1. **Mirror, don't replace.** Every new incoming-edge facility mirrors an existing
+ outgoing-edge facility with a consistent naming convention.
+2. **Backward compatible.** Existing code compiles unchanged. The current `edges()`,
+ `degree()`, etc. continue to mean "outgoing."
+3. **Opt-in.** Graphs that don't store reverse adjacency data simply don't model the
+ new concepts — algorithms gracefully fall back to outgoing-only behavior.
+4. **Consistent naming.** Incoming counterparts use an `in_` prefix; where ambiguity
+ arises, outgoing counterparts gain an `out_` alias.
+5. **Independent edge types.** `in_edge_t` and `edge_t` (i.e. `out_edge_t`)
+ are independently deduced from their respective ranges. They are commonly the same
+ type but this is **not** required. For example, in a distributed graph the outgoing
+ edges may carry full property data while incoming edges are lightweight back-pointers
+ (just a source vertex ID). In a CSR+CSC compressed graph, the CSC incoming entries
+ may store only a column index plus a back-reference into the CSR value array.
+
+---
+
+## 3. Naming Convention
+
+### 3.1 Existing names (no rename required)
+
+The current names remain as-is and **continue to mean "outgoing edges"**:
+
+| Current Name | Stays | Meaning |
+|---|---|---|
+| `edges(g, u)` | Yes | Outgoing edges from vertex `u` |
+| `degree(g, u)` | Yes | Out-degree of `u` |
+| `target_id(g, uv)` | Yes | Target vertex of edge `uv` |
+| `source_id(g, uv)` | Yes | Source vertex of edge `uv` |
+| `target(g, uv)` | Yes | Target vertex descriptor |
+| `source(g, uv)` | Yes | Source vertex descriptor |
+| `num_edges(g)` | Yes | Total edge count (direction-agnostic) |
+| `find_vertex_edge(g, u, v)` | Yes | Find outgoing edge from `u` to `v` |
+| `contains_edge(g, u, v)` | Yes | Check outgoing edge from `u` to `v` |
+| `has_edge(g)` | Yes | Check whether graph has at least one edge |
+| `edge_value(g, uv)` | Yes | Edge property (direction-agnostic) |
+
+### 3.2 New outgoing aliases (optional clarity)
+
+For code that wants to be explicit, introduce **aliases** that forward to the
+existing CPOs. These are convenience only — not required:
+
+| New Alias | Forwards To |
+|---|---|
+| `out_edges(g, u)` | `edges(g, u)` |
+| `out_degree(g, u)` | `degree(g, u)` |
+| `find_out_edge(g, u, v)` | `find_vertex_edge(g, u, v)` |
+
+These are defined as inline constexpr CPO aliases or thin wrapper functions in
+`graph::adj_list` and re-exported to `graph::`.
+
+> **Design decision (2026-02-21):** These aliases are **retained**. See
+> [Appendix D](#appendix-d-out_-alias-retention-decision) for the full trade-off
+> analysis.
+
+### 3.3 New incoming names
+
+| New Name | Meaning |
+|---|---|
+| `in_edges(g, u)` / `in_edges(g, uid)` | Incoming edges to vertex `u` |
+| `in_degree(g, u)` / `in_degree(g, uid)` | In-degree of `u` |
+| `find_in_edge(g, u, v)` | Find incoming edge from `v` to `u` (both descriptors) |
+| `find_in_edge(g, u, vid)` | Find incoming edge from `vid` to `u` (descriptor + ID) |
+| `find_in_edge(g, uid, vid)` | Find incoming edge from `vid` to `uid` (both IDs) |
+| `contains_in_edge(g, u, v)` | Check incoming edge from `v` to `u` (both descriptors) |
+| `contains_in_edge(g, uid, vid)` | Check incoming edge from `vid` to `uid` (both IDs) |
+
+Parameter convention for incoming-edge queries:
+
+- `u`/`uid` = **target** vertex (the vertex receiving incoming edges)
+- `v`/`vid` = **source** vertex (the vertex the edge comes from)
+
+Example:
+
+```cpp
+// true if edge 7 -> 3 exists
+bool b = contains_in_edge(g, /*target=*/3, /*source=*/7);
+```
+
+---
+
+## 4. New CPOs
+
+### 4.1 `in_edges(g, u)` / `in_edges(g, uid)`
+
+**File:** `include/graph/adj_list/detail/graph_cpo.hpp` (new section)
+
+**Resolution order** (mirrors `edges()`):
+
+| Priority | Strategy | Expression |
+|---|---|---|
+| 1 | `_vertex_member` | `u.inner_value(g).in_edges()` |
+| 2 | `_adl` | ADL `in_edges(g, u)` |
+
+For the `(g, uid)` overload:
+
+| Priority | Strategy | Expression |
+|---|---|---|
+| 1 | `_adl` | ADL `in_edges(g, uid)` |
+| 2 | `_default` | `in_edges(g, *find_vertex(g, uid))` |
+
+**Return type:** `in_vertex_edge_range_t` — i.e. the exact range type returned by
+`in_edges(g, u)` after CPO resolution. This may be an `edge_descriptor_view`, but
+custom member/ADL implementations may return a different valid range type.
+
+### 4.2 `in_degree(g, u)` / `in_degree(g, uid)`
+
+**File:** `include/graph/adj_list/detail/graph_cpo.hpp` (new section)
+
+**Resolution order** (mirrors `degree()`):
+
+| Priority | Strategy | Expression |
+|---|---|---|
+| 1 | `_member` | `g.in_degree(u)` |
+| 2 | `_adl` | ADL `in_degree(g, u)` |
+| 3 | `_default` | `size(in_edges(g, u))` or `distance(in_edges(g, u))` |
+
+### 4.3 `out_edges` / `out_degree` / `find_out_edge` (aliases)
+
+**File:** `include/graph/adj_list/detail/graph_cpo.hpp`
+
+```cpp
+inline constexpr auto& out_edges = edges;
+inline constexpr auto& out_degree = degree;
+inline constexpr auto& find_out_edge = find_vertex_edge;
+```
+
+### 4.4 `find_in_edge`, `contains_in_edge`
+
+These mirror `find_vertex_edge` (`find_out_edge`) / `contains_edge` but operate
+on the reverse adjacency structure. Implementation follows the same
+member → ADL → default cascade pattern.
+
+`find_in_edge` has three overloads mirroring `find_vertex_edge`:
+- `find_in_edge(g, u, v)` — both vertex descriptors
+- `find_in_edge(g, u, vid)` — descriptor + vertex ID
+- `find_in_edge(g, uid, vid)` — both vertex IDs
+
+`contains_in_edge` has two overloads mirroring `contains_edge`:
+- `contains_in_edge(g, u, v)` — both vertex descriptors
+- `contains_in_edge(g, uid, vid)` — both vertex IDs
+
+The default implementation of `find_in_edge` iterates `in_edges(g, u)` and
+matches `source_id(g, ie)` against the target vertex (the in-neighbor). The default
+`contains_in_edge` calls `find_in_edge` and checks the result.
+
+> **Note:** There is no `has_in_edge` counterpart. The existing `has_edge(g)` takes
+> only the graph and means "graph has at least one edge" — a direction-agnostic
+> query. A `has_in_edge(g)` variant would be redundant with `has_edge(g)`, and
+> `has_in_edge(g, uid, vid)` would duplicate `contains_in_edge(g, uid, vid)`.
+
+---
+
+## 5. New Type Aliases
+
+**File:** `include/graph/adj_list/detail/graph_cpo.hpp` (after `in_edges` CPO)
+
+| Alias | Definition |
+|---|---|
+| `in_vertex_edge_range_t` | `decltype(in_edges(g, vertex_t))` |
+| `in_vertex_edge_iterator_t` | `ranges::iterator_t>` |
+| `in_edge_t` | `ranges::range_value_t>` |
+
+`in_edge_t` is **independently deduced** from the `in_edges()` return range.
+It is commonly the same type as `edge_t` but this is not required. See
+Design Principle 5 and Appendix C for rationale.
+
+Also add outgoing aliases for explicitness:
+
+| Alias | Definition |
+|---|---|
+| `out_vertex_edge_range_t` | Same as `vertex_edge_range_t` |
+| `out_vertex_edge_iterator_t` | Same as `vertex_edge_iterator_t` |
+| `out_edge_t` | Same as `edge_t` |
+
+---
+
+## 6. New Concepts
+
+**File:** `include/graph/adj_list/adjacency_list_concepts.hpp`
+
+### 6.1 `in_vertex_edge_range`
+
+```cpp
+template
+concept in_vertex_edge_range =
+ std::ranges::forward_range;
+```
+
+Note: this is intentionally **less constrained** than `vertex_edge_range` (which
+requires `edge>`). Incoming edges need only be iterable —
+their element type (`in_edge_t`) may be a lightweight back-pointer that does
+not satisfy the full `edge` concept. The minimum requirement is that
+`source_id(g, ie)` can extract the neighbor vertex ID (see §6.2).
+
+When `in_edge_t` and `edge_t` are the same type, the range will naturally
+model `vertex_edge_range` as well.
+
+### 6.2 `bidirectional_adjacency_list`
+
+```cpp
+template
+concept bidirectional_adjacency_list =
+ adjacency_list &&
+ requires(G& g, vertex_t u, in_edge_t ie) {
+ { in_edges(g, u) } -> in_vertex_edge_range;
+ { source_id(g, ie) } -> std::convertible_to>;
+ // Note: no edge_value() requirement on incoming edges
+ };
+```
+
+A graph that models `bidirectional_adjacency_list` supports both `edges(g, u)`
+(outgoing) and `in_edges(g, u)` (incoming). The only requirement on incoming
+edge elements is that the neighbor vertex ID (the edge's source) can be extracted
+via `source_id()`. Notably, `edge_value(g, ie)` is **not** required — incoming
+edges may be valueless back-pointers.
+
+When `in_edge_t == edge_t`, `edge_value()` will work on incoming edges
+automatically.
+
+### 6.3 `index_bidirectional_adjacency_list`
+
+```cpp
+template
+concept index_bidirectional_adjacency_list =
+ bidirectional_adjacency_list && index_vertex_range;
+```
+
+---
+
+## 7. New Traits
+
+**File:** `include/graph/adj_list/adjacency_list_traits.hpp`
+
+| Trait | Constraint |
+|---|---|
+| `has_in_degree` | `in_degree(g, u)` and `in_degree(g, uid)` both return integral |
+| `has_in_degree_v` | `bool` shorthand |
+| `has_find_in_edge` | `find_in_edge` CPO resolves for graph type `G` |
+| `has_find_in_edge_v` | `bool` shorthand |
+| `has_contains_in_edge` | `contains_in_edge` CPO resolves for graph type `G` |
+| `has_contains_in_edge_v` | `bool` shorthand |
+
+---
+
+## 8. New Views
+
+### 8.1 `in_incidence` view
+
+**File:** `include/graph/views/in_incidence.hpp` (new file)
+
+Mirrors `incidence_view` from `include/graph/views/incidence.hpp`.
+
+| Class | Yields | Description |
+|---|---|---|
+| `in_incidence_view` | `edge_info{sid, uv}` | Iterates incoming edges to a vertex, yielding **source_id** + edge descriptor |
+| `in_incidence_view` | `edge_info{sid, uv, val}` | Same, with value function |
+| `basic_in_incidence_view` | `edge_info{sid}` | Source ID only |
+| `basic_in_incidence_view` | `edge_info{sid, val}` | Source ID + value function |
+
+**Factory functions:**
+```cpp
+namespace graph::views {
+ in_incidence(g, u) // → in_incidence_view
+ in_incidence(g, uid) // → in_incidence_view
+ in_incidence(g, u, evf) // → in_incidence_view
+ in_incidence(g, uid, evf) // → in_incidence_view
+ basic_in_incidence(g, uid) // → basic_in_incidence_view
+ basic_in_incidence(g, uid, evf) // → basic_in_incidence_view
+}
+```
+
+**Key difference from `incidence_view`:** The standard `incidence_view` iterates
+`edges(g, u)` and yields `target_id` per edge. The `in_incidence_view` iterates
+`in_edges(g, u)` and yields `source_id` per edge (the vertex the edge comes from).
+
+**Edge type note:** The `EVF` value function receives `in_edge_t`, which may
+differ from `edge_t`. When incoming edges are valueless back-pointers, the
+`void` (no value function) variants are the natural choice.
+
+### 8.2 `in_neighbors` view
+
+**File:** `include/graph/views/in_neighbors.hpp` (new file)
+
+Mirrors `neighbors_view` from `include/graph/views/neighbors.hpp`.
+
+| Class | Yields | Description |
+|---|---|---|
+| `in_neighbors_view` | `neighbor_info{sid, n}` | Iterates source vertices of incoming edges |
+| `in_neighbors_view` | `neighbor_info{sid, n, val}` | Same, with vertex value function |
+| `basic_in_neighbors_view` | `neighbor_info{sid}` | Source ID only |
+| `basic_in_neighbors_view` | `neighbor_info{sid, val}` | Source ID + value function |
+
+### 8.3 Outgoing aliases (optional)
+
+For symmetry, add aliases in the `graph::views` namespace:
+
+```cpp
+namespace graph::views {
+ inline constexpr auto& out_incidence = incidence;
+ inline constexpr auto& basic_out_incidence = basic_incidence;
+ inline constexpr auto& out_neighbors = neighbors;
+ inline constexpr auto& basic_out_neighbors = basic_neighbors;
+}
+```
+
+### 8.4 Pipe-syntax adaptors
+
+**File:** `include/graph/views/adaptors.hpp` (extend existing)
+
+The existing `adaptors.hpp` provides pipe-syntax closures for all outgoing views
+(`g | incidence(uid)`, `g | neighbors(uid)`, etc.). Corresponding closures must
+be added for the new incoming views:
+
+| Pipe expression | Expands to |
+|---|---|
+| `g \| in_incidence(uid)` | `in_incidence(g, uid)` |
+| `g \| in_incidence(uid, evf)` | `in_incidence(g, uid, evf)` |
+| `g \| basic_in_incidence(uid)` | `basic_in_incidence(g, uid)` |
+| `g \| basic_in_incidence(uid, evf)` | `basic_in_incidence(g, uid, evf)` |
+| `g \| in_neighbors(uid)` | `in_neighbors(g, uid)` |
+| `g \| in_neighbors(uid, vvf)` | `in_neighbors(g, uid, vvf)` |
+| `g \| basic_in_neighbors(uid)` | `basic_in_neighbors(g, uid)` |
+| `g \| basic_in_neighbors(uid, vvf)` | `basic_in_neighbors(g, uid, vvf)` |
+
+Outgoing aliases for symmetry:
+
+| Pipe alias | Forwards to |
+|---|---|
+| `g \| out_incidence(uid)` | `g \| incidence(uid)` |
+| `g \| out_neighbors(uid)` | `g \| neighbors(uid)` |
+| `g \| basic_out_incidence(uid)` | `g \| basic_incidence(uid)` |
+| `g \| basic_out_neighbors(uid)` | `g \| basic_neighbors(uid)` |
+
+---
+
+## 9. BFS/DFS/Topological-Sort Parameterization
+
+### Problem
+
+`bfs.hpp`, `dfs.hpp`, and `topological_sort.hpp` hardcode `adj_list::edges(g, vertex)`
+(5 locations in BFS, 5 in DFS, 7 in topological sort).
+To support reverse traversal (following incoming edges), they need parameterization.
+
+### Solution
+
+Add an **edge accessor** template parameter defaulting to the current outgoing behavior:
+
+```cpp
+// Default edge accessor — current behavior
+struct out_edges_accessor {
+ template
+ constexpr auto operator()(G& g, vertex_t u) const {
+ return adj_list::edges(g, u);
+ }
+};
+
+// Reverse edge accessor
+struct in_edges_accessor {
+ template
+ constexpr auto operator()(G& g, vertex_t u) const {
+ return adj_list::in_edges(g, u);
+ }
+};
+
+template
+class vertices_bfs_view { ... };
+
+template
+class edges_bfs_view { ... };
+```
+
+Factory functions add overloads accepting the accessor:
+```cpp
+// Existing (unchanged)
+vertices_bfs(g, seed)
+vertices_bfs(g, seed, vvf)
+
+// New
+vertices_bfs(g, seed, vvf, in_edges_accessor{}) // reverse BFS
+edges_bfs(g, seed, evf, in_edges_accessor{}) // reverse BFS
+```
+
+Same pattern applies to DFS views (`vertices_dfs_view`, `edges_dfs_view`) and
+topological sort views (`vertices_topological_sort_view`, `edges_topological_sort_view`).
+All three view families receive identical `EdgeAccessor` parameterization.
+
+---
+
+## 10. Container Support
+
+### 10.1 `dynamic_graph` — Reverse Adjacency Storage
+
+The simplest approach is a **separate reverse adjacency list** stored alongside the
+forward list. This is a well-known pattern (e.g., Boost.Graph's `bidirectionalS`).
+
+**Option A: Built-in template parameter**
+
+Add a `bool Bidirectional = false` template parameter to `dynamic_graph_base`:
+
+```cpp
+template
+class dynamic_graph_base;
+```
+
+When `Bidirectional = true`:
+- Each vertex stores two edge containers: `out_edges_` and `in_edges_`
+- `create_edge(u, v)` inserts into `u.out_edges_` and `v.in_edges_`
+- `erase_edge` removes from both lists
+- The `edges(g, u)` CPO returns `u.out_edges_` (unchanged)
+- The `in_edges(g, u)` CPO returns `u.in_edges_`
+
+The in-edge container element type may differ from the out-edge element type.
+The default stores the same `EV` (so `in_edge_t == edge_t`), but an
+additional template parameter (e.g., `InEV = EV`) can allow lightweight
+back-pointer in-edges (e.g., just a source vertex ID) for reduced storage.
+
+When `Bidirectional = false`:
+- Behavior is identical to today (no storage overhead)
+
+**Option B: External reverse index wrapper**
+
+A `reverse_adjacency` adaptor that wraps an existing graph and provides
+`in_edges()` via a separately-constructed reverse index. This avoids modifying
+`dynamic_graph` at all but adds a construction cost:
+
+```cpp
+auto rev = graph::reverse_adjacency(g);
+for (auto [sid, uv] : views::in_incidence(rev, target_vertex)) { ... }
+```
+
+**Recommendation:** Option A for tight integration. Option B as an additional
+utility for read-only reverse queries on existing graphs.
+
+### 10.2 `compressed_graph` — CSC (Compressed Sparse Column) Format
+
+The CSR format stores outgoing edges efficiently. The CSC (Compressed Sparse Column)
+format stores incoming edges. A bidirectional `compressed_graph` would store both:
+
+- `row_index_` + `col_index_` (CSR, outgoing — existing)
+- `col_ptr_` + `row_index_csc_` (CSC, incoming — new)
+
+This is standard in sparse matrix libraries (e.g., Eigen stores both CSR and CSC).
+
+**Implementation:** Add a `bool Bidirectional = false` template parameter. When enabled,
+the constructor builds the CSC representation from the input edges alongside the CSR.
+
+### 10.3 `undirected_adjacency_list` — Already bidirectional
+
+The `undirected_adjacency_list` already stores edges in both endpoint vertex lists
+via `inward_list`/`outward_list` links. For undirected graphs, `edges(g, u)` and
+`in_edges(g, u)` return the **same** range — every edge is both incoming and outgoing.
+
+**Implementation:** Provide `in_edges()` as an ADL friend that returns the same
+`edge_descriptor_view` as `edges()`. Similarly, `in_degree()` returns the same value
+as `degree()`. This allows undirected graphs to model `bidirectional_adjacency_list`
+at zero cost.
+
+### 10.4 New trait type combinations for `dynamic_graph`
+
+For each existing trait file (27 total in `include/graph/container/traits/`), a
+corresponding `b` (bidirectional) variant may be added:
+
+| Existing | Bidirectional Variant | Description |
+|---|---|---|
+| `vov_graph_traits` | `bvov_graph_traits` | Bidirectional vector-of-vectors |
+| `vol_graph_traits` | `bvol_graph_traits` | Bidirectional vector-of-lists |
+| ... | ... | ... |
+
+Alternatively, a single set of traits with `Bidirectional` as a template parameter
+avoids the combinatorial explosion.
+
+### 10.5 Mutator Invariants & Storage/Complexity Budget
+
+To keep the implementation predictable, reverse adjacency support must define and
+enforce container invariants explicitly.
+
+**Mutator invariants (Bidirectional = true):**
+
+1. `create_edge(u, v)` inserts one forward entry in `u.out_edges_` and one reverse
+ entry in `v.in_edges_` representing the same logical edge.
+2. `erase_edge(u, v)` removes both forward and reverse entries.
+3. `erase_vertex(x)` removes all incident edges and corresponding mirrored entries
+ in opposite adjacency lists.
+4. `clear_vertex(x)` removes both outgoing and incoming incidence for `x`.
+5. `clear()` empties both forward and reverse structures.
+6. Copy/move/swap preserve forward↔reverse consistency.
+
+**Complexity/storage targets:**
+
+- Outgoing-only mode (`Bidirectional = false`) remains unchanged.
+- Bidirectional mode targets O(1) additional work per edge insertion/erasure in
+ adjacency-list containers (plus container-specific costs).
+- Storage overhead target is roughly one additional reverse entry per edge
+ (`O(E)` extra), with optional reduced footprint via lightweight `in_edge_t`.
+
+---
+
+## 11. Algorithm Updates
+
+Most algorithms only need outgoing edges and require **no changes**. The following
+would benefit from incoming edge support:
+
+| Algorithm | Benefit |
+|---|---|
+| **Kosaraju's SCC** (`connected_components.hpp`) | Currently simulates reverse graph by rebuilding edges. With `in_edges()`, the second DFS pass can use `in_edges_accessor` directly. |
+| **PageRank** (future) | Requires iterating incoming edges for each vertex. |
+| **Reverse BFS/DFS** | Enable with `in_edges_accessor` parameter (§9). |
+| **Transpose graph** | Can be implemented as a zero-cost view that swaps `edges()`↔`in_edges()`. |
+| **Dominator trees** | Require reverse control flow graph (incoming edges). |
+
+No existing algorithm needs to be **renamed**. They all operate on outgoing
+edges by default and remain unchanged.
+
+---
+
+## 12. Namespace & Re-export Updates
+
+**File:** `include/graph/graph.hpp`
+
+Add to the `graph::` re-exports:
+
+```cpp
+// New incoming-edge CPOs
+using adj_list::in_edges;
+using adj_list::in_degree;
+using adj_list::out_edges;
+using adj_list::out_degree;
+using adj_list::find_out_edge;
+using adj_list::find_in_edge;
+using adj_list::contains_in_edge;
+
+// New concepts
+using adj_list::in_vertex_edge_range;
+using adj_list::bidirectional_adjacency_list;
+using adj_list::index_bidirectional_adjacency_list;
+
+// New traits
+using adj_list::has_in_degree;
+using adj_list::has_in_degree_v;
+
+// New type aliases
+using adj_list::in_vertex_edge_range_t;
+using adj_list::in_vertex_edge_iterator_t;
+using adj_list::in_edge_t;
+using adj_list::out_vertex_edge_range_t;
+using adj_list::out_vertex_edge_iterator_t;
+using adj_list::out_edge_t;
+```
+
+**File:** `include/graph/graph.hpp` — add includes:
+
+```cpp
+#include
+#include
+```
+
+---
+
+## 13. Documentation Updates
+
+### 13.1 Existing docs to update
+
+| File | Change |
+|---|---|
+| `docs/index.md` | Add "Bidirectional edge access" to feature list |
+| `docs/getting-started.md` | Add section on incoming edges; update "outgoing edges" mentions to be explicit |
+| `docs/user-guide/views.md` | Add `in_incidence` and `in_neighbors` views; add `out_incidence`/`out_neighbors` aliases |
+| `docs/user-guide/algorithms.md` | Note which algorithms benefit from bidirectional access |
+| `docs/user-guide/containers.md` | Document bidirectional dynamic_graph and compressed_graph |
+| `docs/reference/concepts.md` | Add `bidirectional_adjacency_list`, `in_vertex_edge_range` |
+| `docs/reference/cpo-reference.md` | Add `in_edges`, `in_degree`, `out_edges`, `out_degree`, `find_in_edge`, `contains_in_edge` |
+| `docs/reference/type-aliases.md` | Add `in_vertex_edge_range_t`, `in_edge_t`, etc. |
+| `docs/reference/adjacency-list-interface.md` | Add incoming edge section |
+| `docs/contributing/cpo-implementation.md` | Add `in_edges` as an example of the CPO pattern |
+| `README.md` | Update feature highlights with bidirectional support |
+
+### 13.2 New docs
+
+| File | Content |
+|---|---|
+| `docs/user-guide/bidirectional-access.md` | Tutorial-style guide on using incoming edges |
+
+---
+
+## 14. Implementation Phases
+
+### Phase 1: Core CPOs & Concepts (~2-3 days)
+
+1. Implement `in_edges` CPO in `graph_cpo.hpp` (mirror `edges` CPO)
+2. Implement `in_degree` CPO (mirror `degree` CPO)
+3. Add `out_edges` / `out_degree` aliases
+4. Define `in_vertex_edge_range_t`, `in_edge_t`, etc.
+5. Define `bidirectional_adjacency_list` concept
+6. Add `has_in_degree` trait
+7. Update `graph.hpp` re-exports
+8. Add unit tests for CPO resolution (stub graph with `in_edges()` member)
+9. Add compile-time concept tests for `bidirectional_adjacency_list`
+10. Add mixed-type tests where `in_edge_t != edge_t` (source-only incoming edges)
+
+### Phase 2: Views (~2 days)
+
+1. Create `in_incidence.hpp` (copy+adapt `incidence.hpp`)
+2. Create `in_neighbors.hpp` (copy+adapt `neighbors.hpp`)
+3. Add `out_incidence` / `out_neighbors` aliases
+4. Add pipe-syntax adaptors to `adaptors.hpp` (§8.4)
+5. Unit tests for all new views (including pipe-syntax adaptor tests)
+
+### Phase 3: Container Support (~3-4 days)
+
+1. Add `Bidirectional` parameter to `dynamic_graph_base`
+2. Update `create_edge` / `erase_edge` / `clear` to maintain reverse lists
+3. Update `erase_vertex` / `clear_vertex` / copy-move-swap paths to preserve reverse invariants
+4. Add ADL `in_edges()` friend to bidirectional dynamic_graph
+5. Add CSC support to `compressed_graph`
+6. Add `in_edges()` to `undirected_adjacency_list` (returns same as `edges()`)
+7. Unit tests for all three containers (including mutator invariant checks)
+
+### Phase 4: BFS/DFS Parameterization (~1-2 days)
+
+1. Add `EdgeAccessor` template parameter to BFS/DFS/topological sort views
+2. Define `out_edges_accessor` and `in_edges_accessor` functors
+3. Add factory function overloads
+4. Unit tests for reverse BFS/DFS
+
+### Phase 5: Algorithm Updates (~1-2 days)
+
+1. Refactor Kosaraju's SCC to use `in_edges()` when available
+2. Add `transpose_view` (zero-cost view swapping `edges()`↔`in_edges()`)
+3. Unit tests
+
+### Phase 6: Documentation (~2 days)
+
+1. Update all docs listed in §13.1
+2. Create `bidirectional-access.md` tutorial
+3. Update CHANGELOG.md
+
+### Recommended execution order (risk-first)
+
+To reduce integration risk and keep PRs reviewable, execute phases with strict
+merge gates and defer high-churn container internals until API contracts are
+proven by tests.
+
+1. **Phase 1 (CPOs/concepts/traits) first, in one or two PRs max.**
+2. **Phase 2 (views) second**, consuming only the new public CPOs.
+3. **Phase 4 (BFS/DFS parameterization) third**, before container internals,
+ to validate the accessor approach on existing graphs.
+4. **Phase 3 (container support) fourth**, split by container:
+ `undirected_adjacency_list` → `dynamic_graph` → `compressed_graph`.
+5. **Phase 5 (algorithm updates) fifth**, after reverse traversal behavior is
+ stable and benchmarked.
+6. **Phase 6 (docs/changelog) last**, plus updates in each phase PR where needed.
+
+### Merge gates (go/no-go criteria)
+
+Each phase must satisfy all gates before proceeding:
+
+- `linux-gcc-debug` full test suite passes.
+- New compile-time concept tests pass (`requires`/`static_assert` coverage).
+- No regressions in existing outgoing-edge APIs (`edges`, `degree`, `find_vertex_edge`).
+- New incoming APIs are documented in reference docs for that phase.
+
+Additional phase-specific gates:
+
+- **After Phase 1:**
+ - Mixed-type incoming-edge test (`in_edge_t != edge_t`) passes.
+ - `in_edges`/`in_degree` ADL and member dispatch paths are covered.
+- **After Phase 4:**
+ - Reverse BFS/DFS outputs validated against expected traversal sets.
+ - Existing BFS/DFS/topological-sort signatures remain source-compatible.
+- **After Phase 3:**
+ - Mutator invariant tests pass (`create_edge`, `erase_edge`, `erase_vertex`,
+ `clear_vertex`, `clear`, copy/move/swap consistency).
+
+### Fallback and scope control
+
+- If CSC integration in `compressed_graph` slips, ship `dynamic_graph` +
+ `undirected_adjacency_list` incoming support first and keep CSC behind a
+ follow-up milestone.
+- Keep `transpose_view` optional for initial release; prioritize core CPOs,
+ views, and reverse traversal correctness.
+- If API pressure grows, keep only the decided aliases (`out_*`) and avoid
+ introducing additional directional synonyms.
+
+---
+
+## 15. Summary of All Changes
+
+| Category | New | Modified | Deleted |
+|---|---|---|---|
+| **CPOs** | `in_edges`, `in_degree`, `out_edges` (alias), `out_degree` (alias), `find_out_edge` (alias), `find_in_edge`, `contains_in_edge` | — | — |
+| **Concepts** | `in_vertex_edge_range`, `bidirectional_adjacency_list`, `index_bidirectional_adjacency_list` | — | — |
+| **Traits** | `has_in_degree`, `has_in_degree_v`, `has_find_in_edge`, `has_contains_in_edge` | — | — |
+| **Type Aliases** | `in_vertex_edge_range_t`, `in_vertex_edge_iterator_t`, `in_edge_t`, `out_vertex_edge_range_t`, `out_vertex_edge_iterator_t`, `out_edge_t` | — | — |
+| **Views** | `in_incidence.hpp`, `in_neighbors.hpp` | `bfs.hpp`, `dfs.hpp`, `topological_sort.hpp` (add `EdgeAccessor` param), `adaptors.hpp` (add pipe closures) | — |
+| **Traversal policies** | `out_edges_accessor`, `in_edges_accessor` | — | — |
+| **Containers** | — | `dynamic_graph.hpp` (add `Bidirectional`), `compressed_graph.hpp` (add CSC), `undirected_adjacency_list.hpp` (add `in_edges` friend) | — |
+| **Algorithms** | `transpose_view` | `connected_components.hpp` (Kosaraju optimization) | — |
+| **Headers** | — | `graph.hpp` (new includes & re-exports) | — |
+| **Docs** | `bidirectional-access.md` | 11 existing docs (§13.1) | — |
+| **Tests** | CPO tests, view tests, container tests, BFS/DFS reverse tests | — | — |
+
+**Estimated effort:** 11–15 days
+
+---
+
+## Appendix A: Full CPO Resolution Table
+
+| CPO | Tier 1 | Tier 2 | Tier 3 |
+|---|---|---|---|
+| `edges(g, u)` | `u.inner_value(g).edges()` | ADL `edges(g, u)` | inner_value is forward_range → wrap |
+| `in_edges(g, u)` | `u.inner_value(g).in_edges()` | ADL `in_edges(g, u)` | *(no default)* |
+| `degree(g, u)` | `g.degree(u)` | ADL `degree(g, u)` | `size(edges(g, u))` |
+| `in_degree(g, u)` | `g.in_degree(u)` | ADL `in_degree(g, u)` | `size(in_edges(g, u))` |
+| `find_vertex_edge(g, u, v)` | `g.find_vertex_edge(u, v)` | ADL `find_vertex_edge(g, u, v)` | iterate `edges(g, u)`, match `target_id` |
+| `find_in_edge(g, u, v)` | `g.find_in_edge(u, v)` | ADL `find_in_edge(g, u, v)` | iterate `in_edges(g, u)`, match `source_id` |
+| `contains_edge(g, u, v)` | `g.contains_edge(u, v)` | ADL `contains_edge(g, u, v)` | iterate `edges(g, u)`, match `target_id` |
+| `contains_in_edge(g, u, v)` | `g.contains_in_edge(u, v)` | ADL `contains_in_edge(g, u, v)` | iterate `in_edges(g, u)`, match `source_id` |
+| `has_edge(g)` | `g.has_edge()` | ADL `has_edge(g)` | iterate vertices, find non-empty edges |
+| `out_edges(g, u)` | → `edges(g, u)` | | |
+| `out_degree(g, u)` | → `degree(g, u)` | | |
+| `find_out_edge(g, u, v)` | → `find_vertex_edge(g, u, v)` | | |
+
+## Appendix B: No-Rename Justification
+
+Several graph libraries (Boost.Graph, NetworkX, LEDA) distinguish `out_edges` /
+`in_edges` explicitly. In graph-v3, `edges()` has **always** meant outgoing (this is
+documented and all algorithms depend on it). Renaming `edges()` to `out_edges()` would:
+
+- Break every existing user's code
+- Require renaming `vertex_edge_range_t` → `out_vertex_edge_range_t` across 50+ files
+- Create churn in all 14 algorithms
+
+Instead we keep `edges()` as the primary outgoing CPO (matching the "default is outgoing"
+convention) and add `out_edges` as a convenience alias for codebases that want explicit
+directionality.
+
+## Appendix C: Independent In/Out Edge Types
+
+`in_edge_t` and `edge_t` are independently deduced:
+
+```cpp
+template using edge_t = ranges::range_value_t>; // from edges(g,u)
+template using in_edge_t = ranges::range_value_t>; // from in_edges(g,u)
+```
+
+They are commonly the same type, but this is **not required**. Scenarios where
+they differ:
+
+| Scenario | `edge_t` | `in_edge_t` |
+|---|---|---|
+| Symmetric (common case) | `pair` | `pair` (same) |
+| Distributed graph | `pair` | `VId` (back-pointer only) |
+| CSR + CSC compressed | Full edge with value | Column index + CSR back-ref |
+| Lightweight reverse index | `pair` | `VId` (source ID only) |
+
+The `bidirectional_adjacency_list` concept (§6.2) only requires `source_id(g, ie)`
+on incoming edges — not `edge_value()`. This means:
+- Algorithms that only need the graph structure (BFS, DFS, SCC) work with any
+ `in_edge_t`.
+- Algorithms that need edge properties on incoming edges (rare) can add their own
+ `requires edge_value(g, in_edge_t{})` constraint.
+- The undirected case (`in_edge_t == edge_t`) is the zero-cost happy path.
+
+Minimum incoming-edge contract for algorithms:
+
+- **Always required:** `in_edges(g, u)`, `source_id(g, ie)`
+- **Conditionally required:** `edge_value(g, ie)` only for algorithms/views that
+ explicitly consume incoming edge properties
+- **Not required:** `target_id(g, ie)` for incoming-only traversal algorithms
+
+## Appendix D: `out_` Alias Retention Decision
+
+**Date:** 2026-02-21
+
+### Question
+
+Should the library provide `out_edges`, `out_degree`, `find_out_edge`,
+`out_incidence`, `out_neighbors` (and their `basic_` variants) as aliases for the
+existing outgoing CPOs/views, or should it omit them entirely?
+
+### Arguments for removing the aliases
+
+| # | Argument |
+|---|---|
+| R1 | `edges()` already means "outgoing" and always has — adding `out_edges()` is redundant and inflates the API surface. |
+| R2 | Two spellings for the same operation create ambiguity: users must learn that `out_edges(g, u)` and `edges(g, u)` are identical. |
+| R3 | Aliases clutter autocomplete, documentation tables, and error messages. |
+| R4 | No existing user code ever spells `out_edges()` today, so removing the aliases breaks nobody. |
+| R5 | If a future rename is ever desired (`edges` → `out_edges`), aliases make that rename harder because both names are already established. |
+
+### Arguments for keeping the aliases
+
+| # | Argument |
+|---|---|
+| K1 | **Symmetry with `in_edges`:** When a codebase uses `in_edges()` alongside `edges()`, the lack of an `out_` counterpart is visually jarring. `out_edges` / `in_edges` reads as a matched pair. |
+| K2 | **Self-documenting code:** `out_edges(g, u)` makes directionality explicit at the call site; `edges(g, u)` requires the reader to know the convention. |
+| K3 | **Familiar vocabulary:** Boost.Graph, NetworkX, LEDA, and the P1709 proposal all provide an `out_edges` name. Users migrating from those libraries expect it. |
+| K4 | **Zero runtime cost:** The aliases are `inline constexpr` references to the existing CPO objects — no code duplication, no template bloat, no additional overload resolution. |
+| K5 | **Grep-ability:** Searching a codebase for `out_edges` immediately reveals all outgoing-edge access; searching for `edges` produces false positives from `in_edges`, `num_edges`, `has_edge`, etc. |
+| K6 | **Non-breaking:** Aliases are purely additive. Users who prefer `edges()` continue to use it unchanged. |
+
+### Resolution
+
+**Keep all `out_` aliases** (CPOs and view factory functions) as designed in §3.2,
+§4.3, and §8.3.
+
+The symmetry (K1), self-documentation (K2), and familiarity (K3) benefits outweigh
+the API-surface concern (R1–R3). The aliases are zero-cost (K4) and non-breaking (K6).
+
+To mitigate confusion (R2), documentation will:
+- Note that `out_edges(g, u)` is an alias for `edges(g, u)` wherever it appears.
+- Use `edges()` as the primary spelling in algorithm implementations.
+- Use `out_edges()` in examples that also use `in_edges()`, to keep the pairing
+ visually clear.
diff --git a/agents/incoming_edges_plan.md b/agents/incoming_edges_plan.md
new file mode 100644
index 0000000..a3b4b1c
--- /dev/null
+++ b/agents/incoming_edges_plan.md
@@ -0,0 +1,1094 @@
+# Incoming Edges — Phased Implementation Plan
+
+**Source:** `agents/incoming_edges_design.md`
+**Branch:** `incoming`
+**Build preset:** `linux-gcc-debug` (full test suite)
+
+---
+
+## How to use this plan
+
+Each phase is a self-contained unit of work that can be completed by an agent in
+one session. Phases are strictly ordered — each depends on the prior phase compiling
+and passing all tests. Within each phase, steps are listed in execution order.
+
+**Conventions used throughout:**
+
+- "Mirror X" means copy the implementation pattern from X, replacing outgoing
+ names/semantics with incoming equivalents.
+- "Full test suite passes" means `cmake --build build/linux-gcc-debug -j$(nproc)`
+ succeeds and `cd build/linux-gcc-debug && ctest --output-on-failure` reports
+ zero failures with no regressions.
+- File paths are relative to the repository root.
+
+---
+
+## Phase 1 — `in_edges` and `in_degree` CPOs + type aliases
+
+**Goal:** Add the two core incoming-edge CPOs, their public instances, the
+outgoing aliases (`out_edges`, `out_degree`, `find_out_edge`), the six new type
+aliases, and a CPO unit-test file proving all resolution tiers work.
+
+**Why first:** Every subsequent phase depends on `in_edges` and `in_edge_t`
+existing. This phase touches exactly one production header and adds one test
+file, so it is low-risk and easy to validate.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/adj_list/detail/graph_cpo.hpp` | Add CPOs, aliases, public instances |
+| `tests/adj_list/CMakeLists.txt` | Register new test file |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `tests/adj_list/cpo/test_in_edges_cpo.cpp` | Unit tests |
+
+### Steps
+
+#### 1.1 Add `in_edges` CPO (graph_cpo.hpp)
+
+Insert a new section **after** the `edges` public instance and type aliases
+(after line ~835, before the `_target` namespace). Follow the exact structural
+pattern of `namespace _edges`:
+
+```
+namespace _cpo_impls {
+ namespace _in_edges {
+ // --- (g, u) overload ---
+ enum class _St_u { _none, _vertex_member, _adl };
+
+ template
+ concept _has_vertex_member_u = is_vertex_descriptor_v<...> &&
+ requires(G& g, const U& u) {
+ { u.inner_value(g).in_edges() } -> std::ranges::forward_range;
+ };
+
+ template
+ concept _has_adl_u = requires(G& g, const U& u) {
+ { in_edges(g, u) } -> std::ranges::forward_range;
+ };
+
+ // NOTE: No _edge_value_pattern tier — in_edges has no implicit default.
+ // A graph MUST explicitly provide in_edges() via member or ADL.
+
+ template
+ [[nodiscard]] consteval _Choice_t<_St_u> _Choose_u() noexcept { ... }
+
+ // --- (g, uid) overload ---
+ enum class _St_uid { _none, _adl, _default };
+ // _has_adl_uid, _has_default_uid (find_vertex + in_edges(g, u))
+ template
+ [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { ... }
+
+ class _fn {
+ // operator()(G&& g, const U& u) — vertex descriptor
+ // operator()(G&& g, const VId& uid) — vertex ID
+ // Both use _wrap_if_needed() to ensure edge_descriptor_view return
+ };
+ }
+}
+```
+
+Key differences from `edges`:
+- Only 2 tiers for `(g, u)`: `_vertex_member` and `_adl`. No `_edge_value_pattern`.
+- The `(g, uid)` default delegates to `in_edges(g, *find_vertex(g, uid))`.
+- The `_has_default_uid` concept checks `_has_adl_u` (not `_has_edge_value_pattern`).
+
+#### 1.2 Add `in_edges` public instance and type aliases
+
+Immediately after the `_in_edges` namespace closing brace:
+
+```cpp
+inline namespace _cpo_instances {
+ inline constexpr _cpo_impls::_in_edges::_fn in_edges{};
+}
+
+template
+using in_vertex_edge_range_t = decltype(in_edges(std::declval(), std::declval>()));
+
+template
+using in_vertex_edge_iterator_t = std::ranges::iterator_t>;
+
+template
+using in_edge_t = std::ranges::range_value_t>;
+```
+
+#### 1.3 Add `in_degree` CPO
+
+Insert after the `in_edges` section, mirroring `namespace _degree`:
+
+```
+namespace _cpo_impls {
+ namespace _in_degree {
+ // (g, u): _member (g.in_degree(u)), _adl, _default (size(in_edges(g,u)))
+ // (g, uid): _member, _adl, _default (*find_vertex then in_degree(g,u))
+ class _fn { ... };
+ }
+}
+
+inline namespace _cpo_instances {
+ inline constexpr _cpo_impls::_in_degree::_fn in_degree{};
+}
+```
+
+Key difference from `degree`: the `_default` tier calls `in_edges(g, u)` instead
+of `edges(g, u)`.
+
+#### 1.4 Add outgoing aliases
+
+After the `in_degree` public instance:
+
+```cpp
+inline namespace _cpo_instances {
+ inline constexpr auto& out_edges = edges;
+ inline constexpr auto& out_degree = degree;
+ inline constexpr auto& find_out_edge = find_vertex_edge;
+}
+```
+
+#### 1.5 Add outgoing type aliases
+
+```cpp
+template
+using out_vertex_edge_range_t = vertex_edge_range_t;
+
+template
+using out_vertex_edge_iterator_t = vertex_edge_iterator_t;
+
+template
+using out_edge_t = edge_t;
+```
+
+#### 1.6 Create test file `tests/adj_list/cpo/test_in_edges_cpo.cpp`
+
+Test the following scenarios (mirror `test_edges_cpo.cpp` structure):
+
+1. **Stub graph with `in_edges()` member** — a minimal struct whose vertex
+ `inner_value(g).in_edges()` returns a `vector`. Verify the CPO
+ dispatches to `_vertex_member` tier and the return is an
+ `edge_descriptor_view`.
+
+2. **Stub graph with ADL `in_edges(g, u)` friend** — verify `_adl` tier.
+
+3. **`(g, uid)` overload with default** — verify `_default` tier delegates
+ through `find_vertex` + `in_edges(g, u)`.
+
+4. **Type alias verification** — `in_vertex_edge_range_t`,
+ `in_vertex_edge_iterator_t`, `in_edge_t` all compile and produce
+ expected types.
+
+5. **Mixed-type test** — a stub graph where `edge_t` is
+ `pair` but `in_edge_t` is just `int`. Verify both
+ aliases are independently deduced.
+
+6. **`out_edges` / `out_degree` / `find_out_edge` aliases** — verify they
+ are the exact same object as `edges` / `degree` / `find_vertex_edge`
+ (use `static_assert(&out_edges == &edges)`).
+
+7. **`in_degree` CPO** — test member, ADL, and default (`size(in_edges)`)
+ resolution tiers.
+
+8. **Outgoing type aliases** — verify `out_edge_t` is `edge_t` etc.
+
+#### 1.7 Register test in CMakeLists
+
+Add to `tests/adj_list/CMakeLists.txt` under the CPOs section:
+
+```cmake
+ cpo/test_in_edges_cpo.cpp
+```
+
+#### 1.8 Build and run full test suite
+
+```bash
+cmake --build build/linux-gcc-debug -j$(nproc)
+cd build/linux-gcc-debug && ctest --output-on-failure
+```
+
+### Merge gate
+
+- [ ] Full test suite passes with zero regressions.
+- [ ] All 8 test scenarios pass.
+- [ ] `in_edge_t != edge_t` mixed-type test passes.
+- [ ] `out_edges` alias identity check passes.
+
+---
+
+## Phase 2 — `find_in_edge` and `contains_in_edge` CPOs + traits
+
+**Goal:** Add the remaining incoming-edge CPOs (`find_in_edge`,
+`contains_in_edge`), the new traits (`has_in_degree`, `has_find_in_edge`,
+`has_contains_in_edge`), and their unit tests.
+
+**Why second:** These CPOs depend on `in_edges` (Phase 1). They complete the
+full incoming-edge CPO surface before concepts are defined.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/adj_list/detail/graph_cpo.hpp` | Add `find_in_edge`, `contains_in_edge` CPOs |
+| `include/graph/adj_list/adjacency_list_traits.hpp` | Add 3 new traits |
+| `tests/adj_list/CMakeLists.txt` | Register new test files |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `tests/adj_list/cpo/test_find_in_edge_cpo.cpp` | Unit tests for `find_in_edge` |
+| `tests/adj_list/cpo/test_contains_in_edge_cpo.cpp` | Unit tests for `contains_in_edge` |
+| `tests/adj_list/traits/test_incoming_edge_traits.cpp` | Trait tests |
+
+### Steps
+
+#### 2.1 Add `find_in_edge` CPO (graph_cpo.hpp)
+
+Mirror `namespace _find_vertex_edge` with three overload groups:
+
+1. `find_in_edge(g, u, v)` — both descriptors. Default iterates
+ `in_edges(g, u)` and matches `source_id(g, ie)` against `vertex_id(g, v)`.
+2. `find_in_edge(g, u, vid)` — descriptor + ID. Default iterates
+ `in_edges(g, u)` and matches `source_id(g, ie)` against `vid`.
+3. `find_in_edge(g, uid, vid)` — both IDs. Default delegates via
+ `*find_vertex(g, uid)`.
+
+Key difference from `find_vertex_edge`: iterates `in_edges` and compares
+`source_id` instead of `target_id`.
+
+```cpp
+inline namespace _cpo_instances {
+ inline constexpr _cpo_impls::_find_in_edge::_fn find_in_edge{};
+}
+```
+
+#### 2.2 Add `contains_in_edge` CPO (graph_cpo.hpp)
+
+Mirror `namespace _contains_edge` with two overload groups:
+
+1. `contains_in_edge(g, u, v)` — both descriptors.
+2. `contains_in_edge(g, uid, vid)` — both IDs.
+
+Default implementations iterate `in_edges(g, u)` and match `source_id`.
+
+```cpp
+inline namespace _cpo_instances {
+ inline constexpr _cpo_impls::_contains_in_edge::_fn contains_in_edge{};
+}
+```
+
+#### 2.3 Add traits (adjacency_list_traits.hpp)
+
+Follow the existing `detail::has_X_impl` → `has_X` → `has_X_v` pattern:
+
+```cpp
+// --- has_in_degree ---
+namespace detail {
+ template
+ concept has_in_degree_impl = requires(G& g, vertex_t u, vertex_id_t uid) {
+ { in_degree(g, u) } -> std::integral;
+ { in_degree(g, uid) } -> std::integral;
+ };
+}
+template concept has_in_degree = detail::has_in_degree_impl;
+template inline constexpr bool has_in_degree_v = has_in_degree;
+
+// --- has_find_in_edge ---
+namespace detail {
+ template
+ concept has_find_in_edge_impl = requires(G& g, vertex_t u, vertex_t v,
+ vertex_id_t uid, vertex_id_t vid) {
+ find_in_edge(g, u, v);
+ find_in_edge(g, u, vid);
+ find_in_edge(g, uid, vid);
+ };
+}
+template concept has_find_in_edge = detail::has_find_in_edge_impl;
+template inline constexpr bool has_find_in_edge_v = has_find_in_edge;
+
+// --- has_contains_in_edge ---
+namespace detail {
+ template
+ concept has_contains_in_edge_impl = requires(G& g, vertex_t u, vertex_t v,
+ vertex_id_t uid, vertex_id_t vid) {
+ { contains_in_edge(g, u, v) } -> std::convertible_to;
+ { contains_in_edge(g, uid, vid) } -> std::convertible_to;
+ };
+}
+template concept has_contains_in_edge = detail::has_contains_in_edge_impl;
+template inline constexpr bool has_contains_in_edge_v = has_contains_in_edge;
+```
+
+#### 2.4 Create test files
+
+**`test_find_in_edge_cpo.cpp`** — test all 3 overloads:
+- Stub graph with ADL `in_edges()` friend returning known edges.
+- Verify `find_in_edge(g, u, v)` finds correct incoming edge by `source_id`.
+- Verify `find_in_edge(g, u, vid)` works.
+- Verify `find_in_edge(g, uid, vid)` delegates through `find_vertex`.
+- Verify not-found case.
+
+**`test_contains_in_edge_cpo.cpp`** — test both overloads:
+- Verify `contains_in_edge(g, u, v)` returns true/false correctly.
+- Verify `contains_in_edge(g, uid, vid)` returns true/false correctly.
+
+**`test_incoming_edge_traits.cpp`** — test all 3 traits:
+- Stub graph that models `in_edges`/`in_degree` → `has_in_degree_v` is true.
+- Plain `vector>` → `has_in_degree_v` is false.
+- Stub graph with `in_edges` + `find_in_edge` → `has_find_in_edge_v` is true.
+- Stub graph with `in_edges` + `contains_in_edge` → `has_contains_in_edge_v` is true.
+
+#### 2.5 Register tests in CMakeLists
+
+Add to `tests/adj_list/CMakeLists.txt`:
+
+```cmake
+ cpo/test_find_in_edge_cpo.cpp
+ cpo/test_contains_in_edge_cpo.cpp
+ traits/test_incoming_edge_traits.cpp
+```
+
+#### 2.6 Build and run full test suite
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] All `find_in_edge` overloads work (member, ADL, default).
+- [ ] All `contains_in_edge` overloads work.
+- [ ] Traits correctly detect presence/absence of incoming-edge CPOs.
+
+---
+
+## Phase 3 — Concepts + namespace re-exports
+
+**Goal:** Define `in_vertex_edge_range`, `bidirectional_adjacency_list`,
+`index_bidirectional_adjacency_list` concepts; update `graph.hpp` with all
+re-exports for Phases 1-3.
+
+**Why third:** Concepts depend on the CPOs and type aliases from Phases 1-2.
+Re-exports should be done once after the complete CPO surface exists.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/adj_list/adjacency_list_concepts.hpp` | Add 3 concepts |
+| `include/graph/graph.hpp` | Add re-exports for all new CPOs, aliases, traits, concepts |
+| `tests/adj_list/CMakeLists.txt` | Register new test file |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `tests/adj_list/concepts/test_bidirectional_concepts.cpp` | Concept tests |
+
+### Steps
+
+#### 3.1 Add concepts (adjacency_list_concepts.hpp)
+
+After the `index_adjacency_list` concept:
+
+```cpp
+template
+concept in_vertex_edge_range = std::ranges::forward_range;
+
+template
+concept bidirectional_adjacency_list =
+ adjacency_list &&
+ requires(G& g, vertex_t u, in_edge_t ie) {
+ { in_edges(g, u) } -> in_vertex_edge_range;
+ { source_id(g, ie) } -> std::convertible_to>;
+ };
+
+template
+concept index_bidirectional_adjacency_list =
+ bidirectional_adjacency_list && index_vertex_range;
+```
+
+#### 3.2 Update graph.hpp re-exports
+
+In the `namespace graph { ... }` using-declaration block, add:
+
+```cpp
+// Incoming-edge CPOs
+using adj_list::in_edges;
+using adj_list::in_degree;
+using adj_list::find_in_edge;
+using adj_list::contains_in_edge;
+
+// Outgoing aliases
+using adj_list::out_edges;
+using adj_list::out_degree;
+using adj_list::find_out_edge;
+
+// Incoming-edge type aliases
+using adj_list::in_vertex_edge_range_t;
+using adj_list::in_vertex_edge_iterator_t;
+using adj_list::in_edge_t;
+
+// Outgoing type aliases
+using adj_list::out_vertex_edge_range_t;
+using adj_list::out_vertex_edge_iterator_t;
+using adj_list::out_edge_t;
+
+// Incoming-edge concepts
+using adj_list::in_vertex_edge_range;
+using adj_list::bidirectional_adjacency_list;
+using adj_list::index_bidirectional_adjacency_list;
+
+// Incoming-edge traits
+using adj_list::has_in_degree;
+using adj_list::has_in_degree_v;
+using adj_list::has_find_in_edge;
+using adj_list::has_find_in_edge_v;
+using adj_list::has_contains_in_edge;
+using adj_list::has_contains_in_edge_v;
+```
+
+#### 3.3 Create test file `tests/adj_list/concepts/test_bidirectional_concepts.cpp`
+
+1. **Stub bidirectional graph** — a struct with both `edges()` and `in_edges()`
+ ADL friends. `static_assert(bidirectional_adjacency_list)`.
+
+2. **Outgoing-only graph** — a `vector>`.
+ `static_assert(!bidirectional_adjacency_list)`.
+
+3. **Index variant** — stub graph with random-access vertices.
+ `static_assert(index_bidirectional_adjacency_list)`.
+
+4. **Mixed-type concept** — stub where `in_edge_t` is just `int`
+ (lightweight back-pointer) but `source_id(g, ie)` works.
+ `static_assert(bidirectional_adjacency_list)`.
+
+5. **Re-export test** — verify `graph::bidirectional_adjacency_list` is
+ accessible (compile-time only).
+
+#### 3.4 Register test and build
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] `bidirectional_adjacency_list` satisfied by stub bidirectional graph.
+- [ ] `bidirectional_adjacency_list` not satisfied by `vector>`.
+- [ ] Mixed-type (`in_edge_t != edge_t`) graph satisfies concept.
+- [ ] All re-exports compile via `#include `.
+
+---
+
+## Phase 4 — `undirected_adjacency_list` incoming-edge support
+
+**Goal:** Add `in_edges()` and `in_degree()` ADL friends to
+`undirected_adjacency_list` that return the same ranges as `edges()` and
+`degree()`, making undirected graphs model `bidirectional_adjacency_list`
+at zero cost.
+
+**Why fourth:** This is the simplest container change (just forwarding to
+existing functions) and provides a real container for integration testing.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/container/undirected_adjacency_list.hpp` | Add `in_edges`, `in_degree` friends |
+| `tests/container/CMakeLists.txt` | Register new test file |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `tests/container/test_undirected_bidirectional.cpp` | Integration tests |
+
+### Steps
+
+#### 4.1 Add ADL friends (undirected_adjacency_list.hpp)
+
+In `base_undirected_adjacency_list`, immediately after the existing `edges()`
+friend functions (around line 1030), add:
+
+```cpp
+ // in_edges(g, u) — for undirected graphs, same as edges(g, u)
+ template
+ requires adj_list::vertex_descriptor_type
+ friend constexpr auto in_edges(graph_type& g, const U& u) noexcept {
+ auto uid = static_cast(u.vertex_id());
+ return g.vertices_[uid].edges(g, uid);
+ }
+ template
+ requires adj_list::vertex_descriptor_type
+ friend constexpr auto in_edges(const graph_type& g, const U& u) noexcept {
+ auto uid = static_cast(u.vertex_id());
+ return g.vertices_[uid].edges(g, uid);
+ }
+```
+
+Also add `in_degree` friends if the class has a `degree()` member/friend.
+Otherwise, the `in_degree` CPO's default tier (`size(in_edges(g, u))`) will
+handle it automatically.
+
+#### 4.2 Create test file
+
+Test using actual `undirected_adjacency_list` instances (e.g., `vov_graph`):
+
+1. **`in_edges` returns same edges as `edges`** — for each vertex, verify
+ `in_edges(g, u)` and `edges(g, u)` yield the same edge set.
+
+2. **`in_degree` equals `degree`** — for each vertex.
+
+3. **Concept satisfaction** — `static_assert(bidirectional_adjacency_list)`.
+
+4. **`find_in_edge` works** — since `in_edges` returns the same range.
+
+5. **`contains_in_edge` works** — mirrors `contains_edge`.
+
+6. Run against multiple trait types (at least `vov` and `vol`).
+
+#### 4.3 Register test and build
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] `undirected_adjacency_list` models `bidirectional_adjacency_list`.
+- [ ] `in_edges(g, u)` and `edges(g, u)` produce identical results.
+
+---
+
+## Phase 5 — `in_incidence` and `in_neighbors` views
+
+**Goal:** Create the incoming-edge view files, their factory functions,
+and tests.
+
+**Why fifth:** Views consume the CPOs from Phases 1-3 and can now also be
+tested against the undirected container from Phase 4.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `tests/views/CMakeLists.txt` | Register new test files |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `include/graph/views/in_incidence.hpp` | `in_incidence_view`, `basic_in_incidence_view`, factory functions |
+| `include/graph/views/in_neighbors.hpp` | `in_neighbors_view`, `basic_in_neighbors_view`, factory functions |
+| `tests/views/test_in_incidence.cpp` | View tests |
+| `tests/views/test_in_neighbors.cpp` | View tests |
+
+### Steps
+
+#### 5.1 Create `include/graph/views/in_incidence.hpp`
+
+Copy `incidence.hpp` and make these changes:
+
+- Rename all classes: `incidence_view` → `in_incidence_view`,
+ `basic_incidence_view` → `basic_in_incidence_view`.
+- Change edge iteration from `edges(g, u)` to `in_edges(g, u)`.
+- Change neighbor ID extraction from `target_id(g, e)` to `source_id(g, e)`.
+- Change `edge_t` references to `in_edge_t`.
+- Change concept constraint from `adjacency_list` to `bidirectional_adjacency_list`.
+- Update all doxygen comments.
+- Update the `edge_info` yield: `{sid, uv}` / `{sid, uv, val}` / `{sid}` / `{sid, val}`.
+
+#### 5.2 Create `include/graph/views/in_neighbors.hpp`
+
+Copy `neighbors.hpp` and apply the same transformation:
+
+- Rename: `neighbors_view` → `in_neighbors_view`.
+- Iterate `in_edges` instead of `edges`.
+- Extract `source_id` instead of `target_id`.
+- Retrieve source vertex via `source(g, e)` instead of `target(g, e)`.
+- Constrain with `bidirectional_adjacency_list`.
+
+#### 5.3 Add outgoing view aliases
+
+At the bottom of `in_incidence.hpp`:
+
+```cpp
+namespace graph::views {
+ inline constexpr auto& out_incidence = incidence;
+ inline constexpr auto& basic_out_incidence = basic_incidence;
+}
+```
+
+At the bottom of `in_neighbors.hpp`:
+
+```cpp
+namespace graph::views {
+ inline constexpr auto& out_neighbors = neighbors;
+ inline constexpr auto& basic_out_neighbors = basic_neighbors;
+}
+```
+
+#### 5.4 Update `graph.hpp`
+
+Add includes:
+
+```cpp
+#include
+#include
+```
+
+#### 5.5 Create test files
+
+**`test_in_incidence.cpp`** — mirror `test_incidence.cpp`:
+- Use undirected_adjacency_list (from Phase 4) as the bidirectional graph.
+- Verify `in_incidence(g, u)` yields `{source_id, edge}` tuples.
+- Verify `in_incidence(g, u, evf)` yields `{source_id, edge, value}`.
+- Verify `basic_in_incidence(g, uid)` yields `{source_id}`.
+- Verify factory function overloads (descriptor and ID).
+- Verify `out_incidence` alias is the same as `incidence`.
+
+**`test_in_neighbors.cpp`** — mirror `test_neighbors.cpp`:
+- Verify `in_neighbors(g, u)` yields `{source_id, vertex}` tuples.
+- Verify `basic_in_neighbors(g, uid)` yields `{source_id}`.
+- Verify `out_neighbors` alias.
+
+#### 5.6 Register tests and build
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] `in_incidence_view` iterates incoming edges, yields `source_id`.
+- [ ] `in_neighbors_view` iterates source vertices.
+- [ ] All factory function overloads work.
+- [ ] Outgoing aliases are identity references.
+
+---
+
+## Phase 6 — Pipe-syntax adaptors
+
+**Goal:** Add pipe-syntax closures for `in_incidence`, `in_neighbors`,
+`basic_in_incidence`, `basic_in_neighbors`, and their `out_` aliases
+to `adaptors.hpp`.
+
+**Why sixth:** Adaptors depend on the view headers from Phase 5.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/views/adaptors.hpp` | Add adaptor closures and factory fns |
+| `tests/views/test_adaptors.cpp` | Add pipe-syntax tests for new views |
+
+### Steps
+
+#### 6.1 Add adaptor closures (adaptors.hpp)
+
+For each new view, follow the existing pattern:
+
+1. `in_incidence_adaptor_closure` — holds `uid` and optional `evf`.
+ `operator|(G&& g, closure)` calls `graph::views::in_incidence(g, uid)` or
+ `graph::views::in_incidence(g, uid, evf)`.
+
+2. `in_incidence_adaptor_fn` — has `operator()(uid)`, `operator()(uid, evf)`,
+ `operator()(g, uid)`, `operator()(g, uid, evf)`.
+
+3. Same for `basic_in_incidence`, `in_neighbors`, `basic_in_neighbors`.
+
+4. Outgoing aliases:
+ ```cpp
+ inline constexpr auto& out_incidence = incidence;
+ inline constexpr auto& basic_out_incidence = basic_incidence;
+ inline constexpr auto& out_neighbors = neighbors;
+ inline constexpr auto& basic_out_neighbors = basic_neighbors;
+ ```
+
+Add to the adaptor namespace block at the bottom:
+
+```cpp
+inline constexpr in_incidence_adaptor_fn in_incidence{};
+inline constexpr basic_in_incidence_adaptor_fn basic_in_incidence{};
+inline constexpr in_neighbors_adaptor_fn in_neighbors{};
+inline constexpr basic_in_neighbors_adaptor_fn basic_in_neighbors{};
+```
+
+#### 6.2 Add tests to `test_adaptors.cpp`
+
+Append new test cases:
+
+```cpp
+TEST_CASE("pipe: g | in_incidence(uid)", "[adaptors][in_incidence]") { ... }
+TEST_CASE("pipe: g | in_incidence(uid, evf)", "[adaptors][in_incidence]") { ... }
+TEST_CASE("pipe: g | basic_in_incidence(uid)", "[adaptors][basic_in_incidence]") { ... }
+TEST_CASE("pipe: g | in_neighbors(uid)", "[adaptors][in_neighbors]") { ... }
+TEST_CASE("pipe: g | basic_in_neighbors(uid)", "[adaptors][in_neighbors]") { ... }
+TEST_CASE("pipe: out_ aliases forward", "[adaptors][aliases]") { ... }
+```
+
+#### 6.3 Build and run full test suite
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] `g | in_incidence(uid)` produces same results as `in_incidence(g, uid)`.
+- [ ] All 8 pipe expressions from design doc §8.4 work.
+
+---
+
+## Phase 7 — BFS/DFS/topological-sort `EdgeAccessor` parameterization
+
+**Goal:** Add an `EdgeAccessor` template parameter to all 6 traversal view
+classes and their factory functions; define `out_edges_accessor` and
+`in_edges_accessor` functors.
+
+**Why seventh:** This is a cross-cutting change to 3 existing view headers.
+It must not break any existing call sites (the default accessor preserves
+current behavior). Having the undirected bidirectional container (Phase 4)
+enables testing reverse traversal on a real graph.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/views/bfs.hpp` | Add `EdgeAccessor` param to `vertices_bfs_view`, `edges_bfs_view`; replace 5 hardcoded `adj_list::edges()` calls |
+| `include/graph/views/dfs.hpp` | Same for `vertices_dfs_view`, `edges_dfs_view`; replace 5 calls |
+| `include/graph/views/topological_sort.hpp` | Same for `vertices_topological_sort_view`, `edges_topological_sort_view`; replace 7 calls |
+| `tests/views/CMakeLists.txt` | Register new test file |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `include/graph/views/edge_accessor.hpp` | `out_edges_accessor`, `in_edges_accessor` definitions |
+| `tests/views/test_reverse_traversal.cpp` | Reverse BFS/DFS tests |
+
+### Steps
+
+#### 7.1 Create `edge_accessor.hpp`
+
+```cpp
+#pragma once
+#include
+
+namespace graph::views {
+
+struct out_edges_accessor {
+ template
+ constexpr auto operator()(G& g, adj_list::vertex_t u) const {
+ return adj_list::edges(g, u);
+ }
+};
+
+struct in_edges_accessor {
+ template
+ constexpr auto operator()(G& g, adj_list::vertex_t u) const {
+ return adj_list::in_edges(g, u);
+ }
+};
+
+} // namespace graph::views
+```
+
+#### 7.2 Parameterize BFS views (bfs.hpp)
+
+For each of `vertices_bfs_view` and `edges_bfs_view`:
+
+1. Add `class EdgeAccessor = out_edges_accessor` as the last template parameter
+ (before `Alloc` if present, or after the value function type).
+
+2. Store `[[no_unique_address]] EdgeAccessor edge_accessor_;` member.
+
+3. Replace every `adj_list::edges(g, v)` or `adj_list::edges(*g_, v)` with
+ `edge_accessor_(g, v)` or `edge_accessor_(*g_, v)`.
+
+4. Add factory function overloads accepting `EdgeAccessor`:
+ ```cpp
+ vertices_bfs(g, seed, vvf, accessor)
+ edges_bfs(g, seed, evf, accessor)
+ ```
+
+5. Existing signatures remain unchanged (default `EdgeAccessor`).
+
+#### 7.3 Parameterize DFS views (dfs.hpp)
+
+Same transformation: 5 `adj_list::edges()` calls → `edge_accessor_()`.
+
+#### 7.4 Parameterize topological sort views (topological_sort.hpp)
+
+Same transformation: 7 `adj_list::edges()` calls → `edge_accessor_()`.
+
+#### 7.5 Create `test_reverse_traversal.cpp`
+
+Using an undirected_adjacency_list (where `in_edges == edges`):
+
+1. **Forward BFS** — `vertices_bfs(g, seed)` produces expected order.
+2. **Reverse BFS** — `vertices_bfs(g, seed, void_fn, in_edges_accessor{})`
+ produces expected order (same as forward for undirected).
+3. **Forward DFS** vs **Reverse DFS** — same pattern.
+4. **Source-compatibility** — existing call sites `vertices_bfs(g, seed)` and
+ `vertices_bfs(g, seed, vvf)` still compile unchanged.
+
+#### 7.6 Build and run full test suite
+
+### Merge gate
+
+- [ ] Full test suite passes (zero regressions in existing BFS/DFS/topo tests).
+- [ ] Reverse BFS/DFS with `in_edges_accessor` produces correct traversal.
+- [ ] Existing factory signatures compile without changes.
+
+---
+
+## Phase 8 — `dynamic_graph` bidirectional support
+
+**Goal:** Add `bool Bidirectional = false` template parameter to
+`dynamic_graph_base`, maintain reverse adjacency lists when enabled,
+provide `in_edges()` ADL friend.
+
+**Why eighth:** This is the most complex container change. All prerequisite
+infrastructure (CPOs, concepts, views) is already proven by earlier phases.
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/container/dynamic_graph.hpp` | Add `Bidirectional` param, reverse storage, in_edges friend, mutator updates |
+| `tests/container/CMakeLists.txt` | Register new test file |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `tests/container/test_dynamic_graph_bidirectional.cpp` | Comprehensive tests |
+
+### Steps
+
+#### 8.1 Add `Bidirectional` template parameter
+
+Change `dynamic_graph_base` signature:
+
+```cpp
+template
+class dynamic_graph_base;
+```
+
+Propagate through `dynamic_graph` and all using-aliases.
+
+#### 8.2 Add reverse edge storage (conditional)
+
+When `Bidirectional = true`, each vertex needs a second edge container.
+Use `if constexpr` or a conditional base class:
+
+```cpp
+// In the vertex type:
+if constexpr (Bidirectional) {
+ edges_type in_edges_; // reverse adjacency container
+}
+```
+
+Add `in_edges()` accessor to the vertex type (conditional on `Bidirectional`).
+
+#### 8.3 Update mutators
+
+Update `create_edge(u, v, ...)`:
+```cpp
+if constexpr (Bidirectional) {
+ // Insert reverse entry in v.in_edges_ with source = u
+}
+```
+
+Similarly update `erase_edge`, `erase_vertex`, `clear_vertex`, `clear`,
+copy constructor, move constructor, and swap. Each must maintain
+forward↔reverse consistency.
+
+#### 8.4 Add `in_edges()` ADL friend
+
+```cpp
+template
+requires adj_list::vertex_descriptor_type
+friend constexpr auto in_edges(graph_type& g, const U& u) noexcept
+ requires (Bidirectional)
+{
+ auto uid = static_cast(u.vertex_id());
+ return g.vertices_[uid].in_edges(g, uid);
+}
+```
+
+#### 8.5 Create comprehensive test file
+
+1. **Basic bidirectional graph construction** — create edges, verify `in_edges`
+ returns expected source vertices.
+
+2. **`in_degree` matches expected** — for each vertex.
+
+3. **Concept satisfaction** — `static_assert(bidirectional_adjacency_list)`.
+
+4. **Mutator invariants** — after `create_edge(u, v)`:
+ - `contains_edge(g, u, v) == true`
+ - `contains_in_edge(g, v, u) == true`
+
+ After `erase_edge(...)`:
+ - Both forward and reverse entries removed.
+
+5. **`erase_vertex`** — removes all incident edges from both directions.
+
+6. **`clear_vertex`** — removes all edges for vertex in both directions.
+
+7. **Copy/move** — verify reverse adjacency is preserved.
+
+8. **Non-bidirectional unchanged** — `Bidirectional=false` graph compiles and
+ works identically to before. `static_assert(!bidirectional_adjacency_list)`.
+
+9. **Views integration** — `in_incidence(g, u)` and `in_neighbors(g, u)` work
+ on bidirectional dynamic_graph.
+
+10. Test with at least 2 trait types (e.g., `vov` and `vol`).
+
+#### 8.6 Register test and build
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] All 6 mutator invariants from design doc §10.5 are tested and pass.
+- [ ] Non-bidirectional mode has identical behavior (no regressions).
+- [ ] `bidirectional_adjacency_list` concept satisfied.
+
+---
+
+## Phase 9 — Algorithm updates (Kosaraju + transpose_view)
+
+**Goal:** Optimize Kosaraju's SCC to use `in_edges()` when available;
+add `transpose_view` as a zero-cost adaptor.
+
+**Why ninth:** Algorithms depend on container support (Phase 8) and the
+edge accessor infrastructure (Phase 7).
+
+### Files to modify
+
+| File | Action |
+|---|---|
+| `include/graph/algorithm/connected_components.hpp` | Add `if constexpr (bidirectional_adjacency_list)` branch using `in_edges_accessor` |
+| `tests/algorithms/CMakeLists.txt` | Register new test file |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `include/graph/views/transpose.hpp` | `transpose_view` wrapper |
+| `tests/algorithms/test_scc_bidirectional.cpp` | SCC tests with bidirectional graph |
+| `tests/views/test_transpose.cpp` | Transpose view tests |
+
+### Steps
+
+#### 9.1 Add `transpose_view`
+
+A lightweight wrapper that swaps `edges()` ↔ `in_edges()`:
+
+```cpp
+template
+class transpose_view {
+ G* g_;
+public:
+ // ADL friends: edges(tv, u) → in_edges(*g_, u)
+ // in_edges(tv, u) → edges(*g_, u)
+ // Forward all other CPOs to underlying graph
+};
+```
+
+#### 9.2 Optimize Kosaraju's SCC
+
+Add a compile-time branch:
+```cpp
+if constexpr (bidirectional_adjacency_list) {
+ // Use in_edges_accessor for second DFS pass
+} else {
+ // Keep existing edge-rebuild approach
+}
+```
+
+#### 9.3 Create tests and build
+
+### Merge gate
+
+- [ ] Full test suite passes.
+- [ ] SCC produces correct results on bidirectional graph.
+- [ ] `transpose_view` swaps edge directions correctly.
+
+---
+
+## Phase 10 — Documentation
+
+**Goal:** Update all documentation per design doc §13.
+
+### Files to modify
+
+Per the table in design doc §13.1 (11 files), plus:
+
+| File | Action |
+|---|---|
+| `docs/index.md` | Add "Bidirectional edge access" feature |
+| `docs/getting-started.md` | Add incoming edges section |
+| `docs/user-guide/views.md` | Add `in_incidence`, `in_neighbors` |
+| `docs/user-guide/algorithms.md` | Note bidirectional benefits |
+| `docs/user-guide/containers.md` | Document `Bidirectional` parameter |
+| `docs/reference/concepts.md` | Add `bidirectional_adjacency_list` |
+| `docs/reference/cpo-reference.md` | Add all incoming CPOs |
+| `docs/reference/type-aliases.md` | Add `in_edge_t` etc. |
+| `docs/reference/adjacency-list-interface.md` | Add incoming edge section |
+| `docs/contributing/cpo-implementation.md` | Add `in_edges` example |
+| `README.md` | Update feature highlights |
+| `CHANGELOG.md` | Add entry |
+
+### Files to create
+
+| File | Content |
+|---|---|
+| `docs/user-guide/bidirectional-access.md` | Tutorial guide |
+
+### Steps
+
+1. Update each file per the table.
+2. Create the tutorial guide with examples using `in_edges`, `in_incidence`,
+ reverse BFS, and bidirectional `dynamic_graph`.
+3. Add CHANGELOG entry.
+4. Build docs and verify no broken links.
+
+### Merge gate
+
+- [ ] All docs updated.
+- [ ] Tutorial compiles (code examples are tested).
+- [ ] No broken internal links.
+
+---
+
+## Phase summary
+
+| Phase | Title | New files | Modified files | Key deliverable |
+|---|---|---|---|---|
+| 1 | `in_edges`/`in_degree` CPOs + aliases | 1 test | 2 | Core CPOs + type aliases |
+| 2 | `find_in_edge`/`contains_in_edge` + traits | 3 tests | 3 | Complete CPO surface |
+| 3 | Concepts + re-exports | 1 test | 2 | `bidirectional_adjacency_list` concept |
+| 4 | Undirected container support | 1 test | 1 | First real bidirectional container |
+| 5 | `in_incidence`/`in_neighbors` views | 2 headers + 2 tests | 1 | Incoming-edge views |
+| 6 | Pipe-syntax adaptors | — | 2 | `g \| in_incidence(uid)` |
+| 7 | BFS/DFS/topo EdgeAccessor | 1 header + 1 test | 3 | Reverse traversal |
+| 8 | `dynamic_graph` bidirectional | 1 test | 1 | Directed bidirectional container |
+| 9 | Algorithms | 2 headers + 2 tests | 1 | Kosaraju + transpose |
+| 10 | Documentation | 1 guide | 12 | Complete docs |
+
+**Total estimated effort:** 11-15 days (same as design doc estimate)
+
+---
+
+## Safety principles
+
+1. **Each phase adds only; nothing is removed or renamed.**
+ Existing tests pass between every phase.
+
+2. **Default template arguments preserve source compatibility.**
+ `EdgeAccessor = out_edges_accessor` means no existing call site changes.
+
+3. **Stub graphs in early phases isolate CPO testing from container code.**
+ Phases 1-3 don't touch any container — they use lightweight test stubs.
+
+4. **The simplest container change comes first (Phase 4).**
+ Undirected just forwards to the existing `edges()` function.
+
+5. **The most complex change (Phase 8) comes last in the implementation
+ sequence**, after all the infrastructure it depends on is proven.
+
+6. **Each phase has an explicit merge gate** — a checklist of conditions
+ that must pass before proceeding.
diff --git a/docs/archive/edge_map_analysis.md b/docs/archive/edge_map_analysis.md
index cb036b0..b2165e0 100644
--- a/docs/archive/edge_map_analysis.md
+++ b/docs/archive/edge_map_analysis.md
@@ -288,7 +288,7 @@ if constexpr (is_map_based_edge_container) {
### 4.1 Minimal Viable Changes
-**Required for Phase 4.3.2 (voem_graph_traits):**
+**Required for Phase 4.3.2 (vom_graph_traits):**
1. **Add `is_map_based_edge_container` concept** (container_utility.hpp)
```cpp
@@ -331,7 +331,7 @@ These can be added later in Phase 4.3:
### 4.3 Testing Strategy
-For `voem_graph_traits` (vector vertices + map edges):
+For `vom_graph_traits` (vector vertices + map edges):
1. **Basic functionality tests:**
- Edge insertion (automatic target_id keying)
@@ -346,7 +346,7 @@ For `voem_graph_traits` (vector vertices + map edges):
- `contains_edge(g, uid, vid)` works correctly
3. **Comparison with existing traits:**
- - voem should behave like vov except:
+ - vom should behave like vov except:
- No parallel edges (map uniqueness)
- Edges sorted by target_id
- O(log n) lookup instead of O(n)
@@ -405,7 +405,7 @@ For `voem_graph_traits` (vector vertices + map edges):
**Problem:** Map uniqueness means only one edge per target, even if multiple edges should exist.
**Mitigation:**
-- Document this limitation clearly in voem/moem trait headers
+- Document this limitation clearly in vom/mom trait headers
- For multigraphs, users should use vector/list/set edge containers
- This is a feature, not a bug: map-based edges are for graphs with unique edges
@@ -444,21 +444,21 @@ For typical edge (1-2 VId members): ~8-16 bytes overhead per edge
- [x] Design edge_descriptor changes for map-based edges
- [x] Document findings in edge_map_analysis.md
-### Phase 4.3.2 (voem_graph_traits) - ⏳ PENDING
+### Phase 4.3.2 (vom_graph_traits) - ⏳ PENDING
- [ ] Add `is_map_based_edge_container` concept to container_utility.hpp
- [ ] Add `emplace_edge` helper function to container_utility.hpp
- [ ] Update `edge_descriptor::target_id()` to handle map pair unwrapping
- [ ] Update `load_edges` functions to use map-aware edge construction
-- [ ] Create voem_graph_traits.hpp
-- [ ] Create test_dynamic_graph_voem.cpp with basic tests
-- [ ] Create test_dynamic_graph_cpo_voem.cpp with CPO tests
+- [ ] Create vom_graph_traits.hpp
+- [ ] Create test_dynamic_graph_vom.cpp with basic tests
+- [ ] Create test_dynamic_graph_cpo_vom.cpp with CPO tests
- [ ] Verify all tests pass
-### Phase 4.3.3 (moem_graph_traits) - ⏳ PENDING
+### Phase 4.3.3 (mom_graph_traits) - ⏳ PENDING
-- [ ] Create moem_graph_traits.hpp (reuse voem infrastructure)
-- [ ] Create test files for moem
+- [ ] Create mom_graph_traits.hpp (reuse vom infrastructure)
+- [ ] Create test files for mom
- [ ] Verify all tests pass
## 9. Conclusion
@@ -468,13 +468,13 @@ For typical edge (1-2 VId members): ~8-16 bytes overhead per edge
1. **Container Utility:** Add concept detection and helper for map pair construction
2. **Edge Descriptor:** Update target_id extraction to unwrap map pairs
3. **Load Functions:** Add conditional logic for map-based edge insertion
-4. **Trait Headers:** Define new voem/moem traits with `std::map`
+4. **Trait Headers:** Define new vom/mom traits with `std::map`
**Estimated Effort:**
- Container utility changes: ~50 lines
- Edge descriptor updates: ~20 lines
- Load function updates: ~30 lines per load variant
-- Trait headers: ~40 lines each (voem, moem)
+- Trait headers: ~40 lines each (vom, mom)
- Test files: ~2000 lines per trait (existing template)
**Total:** ~200 lines of core changes + ~4000 lines of tests for two new traits.
diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg
new file mode 100644
index 0000000..6eb18af
--- /dev/null
+++ b/docs/assets/logo.svg
@@ -0,0 +1,52 @@
+
\ No newline at end of file
diff --git a/docs/contributing/architecture.md b/docs/contributing/architecture.md
index 704fe67..0ac4f4f 100644
--- a/docs/contributing/architecture.md
+++ b/docs/contributing/architecture.md
@@ -41,7 +41,7 @@ graph-v3/
│ │ ├── jaccard.hpp
│ │ └── traversal_common.hpp
│ ├── container/ # Library-provided containers
-│ │ ├── dynamic_graph.hpp # 26 trait combinations
+│ │ ├── dynamic_graph.hpp # 27 trait combinations
│ │ ├── compressed_graph.hpp # CSR format
│ │ ├── undirected_adjacency_list.hpp
│ │ ├── container_utility.hpp
@@ -206,7 +206,7 @@ Tests mirror the source structure:
|----------------|---------------|
| `tests/adj_list/` | Descriptor construction, CPO dispatch, type aliases |
| `tests/algorithms/` | One test file per algorithm (Dijkstra, BFS, DFS, …) |
-| `tests/container/` | Container conformance — all 26 trait combinations |
+| `tests/container/` | Container conformance — all 27 trait combinations |
| `tests/edge_list/` | Edge list model tests |
| `tests/views/` | View iteration and composition |
| `tests/common/` | Shared test utilities and helpers |
diff --git a/docs/contributing/cpo-implementation.md b/docs/contributing/cpo-implementation.md
index 5b4c00f..8c107fd 100644
--- a/docs/contributing/cpo-implementation.md
+++ b/docs/contributing/cpo-implementation.md
@@ -829,6 +829,7 @@ strategy:
8. **Mark `[[nodiscard]]`** on CPOs that return values.
9. **Mark `constexpr`** on all CPOs — enable compile-time evaluation where possible.
10. **Document compiler workarounds** if any are needed, with `#if defined(_MSC_VER)` guards.
+11. **Use `decltype(auto)` for value CPOs** — the three value CPOs (`vertex_value`, `edge_value`, `graph_value`) must return `decltype(auto)` so that the exact return type of the resolved member/ADL/default function is preserved. Return-by-value (`T`), return-by-reference (`T&`), return-by-const-reference (`const T&`), and return-by-rvalue-reference (`T&&`) must all be forwarded faithfully without decay or copy.
### `_Fake_copy_init` Pattern (Advanced)
@@ -938,6 +939,7 @@ For each new CPO:
- [ ] `_fn::operator()` uses `if constexpr` chain (no separate overloads)
- [ ] `noexcept` specification references `_Choice._No_throw`
- [ ] `[[nodiscard]]` on operator()
+- [ ] Value CPOs use `decltype(auto)` return type to preserve by-value/by-ref/by-const-ref/by-rvalue-ref semantics
- [ ] `static_assert` with helpful message in the `_none` branch
- [ ] `std::remove_cvref_t` used consistently for `_Choice` lookups
- [ ] Type alias defined after the CPO (if applicable)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 54d470f..306170a 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -112,7 +112,7 @@ Vertices: 4
### Using `dynamic_graph`
For richer features — edge values, flexible container selection, partitioning —
-use `dynamic_graph` with one of the 26 trait combinations.
+use `dynamic_graph` with one of the 27 trait combinations.
```cpp
#include
diff --git a/docs/index.md b/docs/index.md
index 897c9bd..64b926a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,7 +1,14 @@
+
+ |
+
+
# graph-v3 Documentation
> A modern C++20 graph library — 13 algorithms, 7 lazy views, 3 containers, 4261+ tests.
+ |
+
+
---
## For Users
@@ -9,7 +16,7 @@
- [Getting Started](getting-started.md) — installation, first graph, first algorithm
- [Adjacency Lists](user-guide/adjacency-lists.md) — range-of-ranges model, concepts, CPOs
- [Edge Lists](user-guide/edge-lists.md) — flat sourced-edge model, concepts, patterns
-- [Containers](user-guide/containers.md) — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list`, 26 trait combinations
+- [Containers](user-guide/containers.md) — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list`, 27 trait combinations
- [Views](user-guide/views.md) — lazy traversal views (BFS, DFS, topological sort, etc.)
- [Algorithms](user-guide/algorithms.md) — Dijkstra, Bellman-Ford, MST, connected components, and more
@@ -39,3 +46,4 @@
- [FAQ](FAQ.md) — common questions and answers
- [Migration from v2](migration-from-v2.md) — what changed from graph-v2 and how to migrate
- [Status & Metrics](status/metrics.md) — canonical counts and implementation matrix
+- [Code Coverage](status/coverage.md) — line and function coverage report (95.8% lines, 92.0% functions)
diff --git a/docs/migration-from-v2.md b/docs/migration-from-v2.md
index 20bd9fe..141c1e6 100644
--- a/docs/migration-from-v2.md
+++ b/docs/migration-from-v2.md
@@ -50,7 +50,7 @@ edges without holding references to the underlying container.
- Vertex storage in `map` and `unordered_map` (for sparse vertex IDs).
- Edge storage in `map`, `set`, `unordered_set` (for sorted or deduplicated edges).
- Non-integral vertex IDs.
- - 26 vertex×edge container combinations via traits (see
+ - 27 vertex×edge container combinations via traits (see
[Containers](user-guide/containers.md)).
### Graph Container Interface
diff --git a/docs/reference/cpo-reference.md b/docs/reference/cpo-reference.md
index e06767e..2996cd6 100644
--- a/docs/reference/cpo-reference.md
+++ b/docs/reference/cpo-reference.md
@@ -35,9 +35,9 @@ All CPOs listed below are available in `namespace graph` after
| `find_vertex_edge` | `(g, u, v)` / `(g, uid, vid)` | `vertex_edge_iterator_t` | O(deg) | Yes |
| `contains_edge` | `(g, uid, vid)` | `bool` | O(deg) | Yes |
| `has_edge` | `(g)` | `bool` | O(V) | Yes |
-| `vertex_value` | `(g, u)` | deduced | O(1) | No |
-| `edge_value` | `(g, uv)` | deduced | O(1) | Yes |
-| `graph_value` | `(g)` | deduced | O(1) | No |
+| `vertex_value` | `(g, u)` | `decltype(auto)` | O(1) | No |
+| `edge_value` | `(g, uv)` | `decltype(auto)` | O(1) | Yes |
+| `graph_value` | `(g)` | `decltype(auto)` | O(1) | No |
| `partition_id` | `(g, u)` | `partition_id_t` | O(1) | No |
| `num_partitions` | `(g)` | integral | O(1) | Yes (1) |
@@ -253,19 +253,31 @@ Tests whether the graph has at least one edge.
## Value CPOs
+All three value CPOs (`vertex_value`, `edge_value`, `graph_value`) return
+`decltype(auto)`, which **preserves the exact return type** of the resolved
+member, ADL, or default function. Return-by-value (`T`),
+return-by-reference (`T&`), return-by-const-reference (`const T&`), and
+return-by-rvalue-reference (`T&&`) are all faithfully forwarded without
+decay or copy.
+
### `vertex_value(g, u)`
```cpp
-auto vertex_value(G& g, vertex_t& u) -> /* deduced */;
+auto vertex_value(G& g, vertex_t& u) -> /* decltype(auto) */;
```
Returns the user-defined value associated with vertex `u`. **No default** —
the graph must provide this via member or ADL.
+| Property | Value |
+|----------|-------|
+| **Return type** | `decltype(auto)` — preserves by-value, by-ref, by-const-ref, and by-rvalue-ref |
+| **Complexity** | O(1) |
+
### `edge_value(g, uv)`
```cpp
-auto edge_value(G& g, edge_t& uv) -> /* deduced */;
+auto edge_value(G& g, edge_t& uv) -> /* decltype(auto) */;
```
Returns the user-defined value associated with edge `uv`.
@@ -273,17 +285,23 @@ Returns the user-defined value associated with edge `uv`.
| Property | Value |
|----------|-------|
| **Default** | Resolution chain: `uv.edge_value(g)` → ADL → descriptor → `uv.value` member → tuple `get<1>(uv)` (adj_list) / `get<2>(uv)` (edge list) |
+| **Return type** | `decltype(auto)` — preserves by-value, by-ref, by-const-ref, and by-rvalue-ref |
| **Complexity** | O(1) |
### `graph_value(g)`
```cpp
-auto graph_value(G& g) -> /* deduced */;
+auto graph_value(G& g) -> /* decltype(auto) */;
```
Returns the user-defined value associated with the graph itself. **No
default** — the graph must provide this.
+| Property | Value |
+|----------|-------|
+| **Return type** | `decltype(auto)` — preserves by-value, by-ref, by-const-ref, and by-rvalue-ref |
+| **Complexity** | O(1) |
+
---
## Partition CPOs
diff --git a/docs/status/coverage.md b/docs/status/coverage.md
new file mode 100644
index 0000000..9872b8d
--- /dev/null
+++ b/docs/status/coverage.md
@@ -0,0 +1,126 @@
+# Code Coverage Report
+
+> **Generated:** 2026-02-21 | **Compiler:** GCC 15.1.0 | **Preset:** `linux-gcc-coverage`
+> **Tests:** 4261 passed, 0 failed (100% pass rate)
+> **Overall line coverage:** 95.8% (3300 / 3446 lines)
+> **Overall function coverage:** 92.0% (27193 / 29567 functions)
+
+---
+
+## Summary by Category
+
+| Category | Lines Hit | Lines Total | Line Coverage |
+|----------|-----------|-------------|---------------|
+| Adjacency list infrastructure | 321 | 321 | **100.0%** |
+| Algorithms | 739 | 788 | **93.8%** |
+| Containers | 902 | 962 | **93.8%** |
+| Detail / CPOs | 21 | 21 | **100.0%** |
+| Edge list | 15 | 16 | **93.8%** |
+| Views | 1302 | 1336 | **97.5%** |
+
+---
+
+## Detailed File Coverage
+
+### Adjacency List Infrastructure (`adj_list/`)
+
+All adjacency list descriptor and CPO support headers reach **100% line coverage**.
+
+| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % |
+|------|-------------------|--------|-------------------|--------|
+| `descriptor_traits.hpp` | 10 / 10 | 100.0% | 8 / 8 | 100.0% |
+| `detail/graph_cpo.hpp` | 135 / 135 | 100.0% | 4250 / 4253 | 99.9% |
+| `edge_descriptor.hpp` | 74 / 74 | 100.0% | 1205 / 1205 | 100.0% |
+| `edge_descriptor_view.hpp` | 37 / 37 | 100.0% | 2291 / 2292 | 100.0% |
+| `vertex_descriptor.hpp` | 35 / 35 | 100.0% | 1305 / 1306 | 99.9% |
+| `vertex_descriptor_view.hpp` | 30 / 30 | 100.0% | 2321 / 2390 | 97.1% |
+
+### Algorithms (`algorithm/`)
+
+| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % |
+|------|-------------------|--------|-------------------|--------|
+| `articulation_points.hpp` | 53 / 53 | 100.0% | 2 / 2 | 100.0% |
+| `bellman_ford_shortest_paths.hpp` | 54 / 63 | 85.7% | 31 / 31 | 100.0% |
+| `biconnected_components.hpp` | 68 / 68 | 100.0% | 4 / 4 | 100.0% |
+| `breadth_first_search.hpp` | 29 / 29 | 100.0% | 12 / 12 | 100.0% |
+| `connected_components.hpp` | 123 / 137 | 89.8% | 10 / 10 | 100.0% |
+| `depth_first_search.hpp` | 38 / 38 | 100.0% | 5 / 5 | 100.0% |
+| `dijkstra_shortest_paths.hpp` | 54 / 65 | 83.1% | 32 / 36 | 88.9% |
+| `jaccard.hpp` | 24 / 24 | 100.0% | 5 / 5 | 100.0% |
+| `label_propagation.hpp` | 66 / 69 | 95.7% | 3 / 3 | 100.0% |
+| `mis.hpp` | 25 / 28 | 89.3% | 1 / 1 | 100.0% |
+| `mst.hpp` | 117 / 126 | 92.9% | 24 / 24 | 100.0% |
+| `tc.hpp` | 27 / 27 | 100.0% | 2 / 2 | 100.0% |
+| `topological_sort.hpp` | 52 / 52 | 100.0% | 7 / 7 | 100.0% |
+| `traversal_common.hpp` | 9 / 9 | 100.0% | 3 / 3 | 100.0% |
+
+### Containers (`container/`)
+
+| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % |
+|------|-------------------|--------|-------------------|--------|
+| `compressed_graph.hpp` | 198 / 223 | 88.8% | 580 / 606 | 95.7% |
+| `container_utility.hpp` | 6 / 6 | 100.0% | 382 / 382 | 100.0% |
+| `detail/undirected_adjacency_list_impl.hpp` | 312 / 321 | 97.2% | 158 / 163 | 96.9% |
+| `dynamic_graph.hpp` | 286 / 311 | 92.0% | 11429 / 13626 | 83.9% |
+| `undirected_adjacency_list.hpp` | 100 / 101 | 99.0% | 126 / 128 | 98.4% |
+
+### Views (`views/`)
+
+| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % |
+|------|-------------------|--------|-------------------|--------|
+| `adaptors.hpp` | 109 / 109 | 100.0% | 85 / 85 | 100.0% |
+| `bfs.hpp` | 211 / 227 | 93.0% | 146 / 157 | 93.0% |
+| `dfs.hpp` | 225 / 239 | 94.1% | 257 / 268 | 95.9% |
+| `edgelist.hpp` | 219 / 221 | 99.1% | 556 / 578 | 96.2% |
+| `incidence.hpp` | 89 / 89 | 100.0% | 463 / 469 | 98.7% |
+| `neighbors.hpp` | 102 / 102 | 100.0% | 282 / 286 | 98.6% |
+| `search_base.hpp` | 5 / 5 | 100.0% | 11 / 11 | 100.0% |
+| `topological_sort.hpp` | 230 / 232 | 99.1% | 248 / 256 | 96.9% |
+| `vertexlist.hpp` | 112 / 112 | 100.0% | 467 / 469 | 99.6% |
+
+### Other
+
+| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % |
+|------|-------------------|--------|-------------------|--------|
+| `detail/edge_cpo.hpp` | 21 / 21 | 100.0% | 461 / 461 | 100.0% |
+| `edge_list/edge_list_descriptor.hpp` | 15 / 16 | 93.8% | 21 / 21 | 100.0% |
+| `graph_info.hpp` | 0 / 2 | 0.0% | 0 / 2 | 0.0% |
+
+---
+
+## Coverage Gaps
+
+Files below 90% line coverage, ranked by gap size:
+
+| File | Line % | Lines Missed | Notes |
+|------|--------|--------------|-------|
+| `graph_info.hpp` | 0.0% | 2 | Metadata-only header; no executable paths exercised |
+| `dijkstra_shortest_paths.hpp` | 83.1% | 11 | Some overload / error-path branches not reached |
+| `bellman_ford_shortest_paths.hpp` | 85.7% | 9 | Negative-cycle detection path not fully exercised |
+| `compressed_graph.hpp` | 88.8% | 25 | Some construction / accessor overloads untested |
+| `connected_components.hpp` | 89.8% | 14 | Sparse-graph and edge-case branches |
+| `mis.hpp` | 89.3% | 3 | Minor branch not reached |
+
+---
+
+## How to Reproduce
+
+```bash
+# Configure with coverage preset
+cmake --preset linux-gcc-coverage
+
+# Build
+cmake --build --preset linux-gcc-coverage
+
+# Run tests
+ctest --preset linux-gcc-coverage
+
+# Generate HTML report (output in build/linux-gcc-coverage/coverage_html/)
+cd build/linux-gcc-coverage
+lcov --directory . --capture --output-file coverage.info --ignore-errors mismatch
+lcov --remove coverage.info '/usr/*' '/opt/*' '*/tests/*' '*/build/*' '*/_deps/*' \
+ --output-file coverage.info --ignore-errors mismatch
+genhtml coverage.info --output-directory coverage_html --ignore-errors mismatch
+```
+
+Open `build/linux-gcc-coverage/coverage_html/index.html` in a browser for the full interactive report.
diff --git a/docs/status/implementation_matrix.md b/docs/status/implementation_matrix.md
index eb9d8ec..6e6d985 100644
--- a/docs/status/implementation_matrix.md
+++ b/docs/status/implementation_matrix.md
@@ -82,9 +82,10 @@ Utility header: `container_utility.hpp`.
| `std::list` | Bidirectional | O(1) insertion/removal anywhere | `l` |
| `std::set` | Bidirectional | Sorted, deduplicated | `s` |
| `std::unordered_set` | Forward | Hash-based, O(1) avg lookup | `us` |
-| `std::map` | Bidirectional | Sorted by target_id key | `em` |
+| `std::map` | Bidirectional | Sorted by target_id key | `m` |
+| `std::unordered_map` | Forward | Hash-based, O(1) avg lookup by target_id | `um` |
-### All 26 trait files
+### All 27 trait files
Naming convention: `{vertex}o{edge}_graph_traits.hpp`
@@ -96,26 +97,27 @@ Naming convention: `{vertex}o{edge}_graph_traits.hpp`
| 4 | `vol_graph_traits.hpp` | vector | list |
| 5 | `vos_graph_traits.hpp` | vector | set |
| 6 | `vous_graph_traits.hpp` | vector | unordered_set |
-| 7 | `voem_graph_traits.hpp` | vector | map |
-| 8 | `dov_graph_traits.hpp` | deque | vector |
-| 9 | `dod_graph_traits.hpp` | deque | deque |
-| 10 | `dofl_graph_traits.hpp` | deque | forward_list |
-| 11 | `dol_graph_traits.hpp` | deque | list |
-| 12 | `dos_graph_traits.hpp` | deque | set |
-| 13 | `dous_graph_traits.hpp` | deque | unordered_set |
-| 14 | `mov_graph_traits.hpp` | map | vector |
-| 15 | `mod_graph_traits.hpp` | map | deque |
-| 16 | `mofl_graph_traits.hpp` | map | forward_list |
-| 17 | `mol_graph_traits.hpp` | map | list |
-| 18 | `mos_graph_traits.hpp` | map | set |
-| 19 | `mous_graph_traits.hpp` | map | unordered_set |
-| 20 | `moem_graph_traits.hpp` | map | map |
-| 21 | `uov_graph_traits.hpp` | unordered_map | vector |
-| 22 | `uod_graph_traits.hpp` | unordered_map | deque |
-| 23 | `uofl_graph_traits.hpp` | unordered_map | forward_list |
-| 24 | `uol_graph_traits.hpp` | unordered_map | list |
-| 25 | `uos_graph_traits.hpp` | unordered_map | set |
-| 26 | `uous_graph_traits.hpp` | unordered_map | unordered_set |
+| 7 | `vom_graph_traits.hpp` | vector | map |
+| 8 | `voum_graph_traits.hpp` | vector | unordered_map |
+| 9 | `dov_graph_traits.hpp` | deque | vector |
+| 10 | `dod_graph_traits.hpp` | deque | deque |
+| 11 | `dofl_graph_traits.hpp` | deque | forward_list |
+| 12 | `dol_graph_traits.hpp` | deque | list |
+| 13 | `dos_graph_traits.hpp` | deque | set |
+| 14 | `dous_graph_traits.hpp` | deque | unordered_set |
+| 15 | `mov_graph_traits.hpp` | map | vector |
+| 16 | `mod_graph_traits.hpp` | map | deque |
+| 17 | `mofl_graph_traits.hpp` | map | forward_list |
+| 18 | `mol_graph_traits.hpp` | map | list |
+| 19 | `mos_graph_traits.hpp` | map | set |
+| 20 | `mous_graph_traits.hpp` | map | unordered_set |
+| 21 | `mom_graph_traits.hpp` | map | map |
+| 22 | `uov_graph_traits.hpp` | unordered_map | vector |
+| 23 | `uod_graph_traits.hpp` | unordered_map | deque |
+| 24 | `uofl_graph_traits.hpp` | unordered_map | forward_list |
+| 25 | `uol_graph_traits.hpp` | unordered_map | list |
+| 26 | `uos_graph_traits.hpp` | unordered_map | set |
+| 27 | `uous_graph_traits.hpp` | unordered_map | unordered_set |
---
diff --git a/docs/status/metrics.md b/docs/status/metrics.md
index 53191ab..25551b5 100644
--- a/docs/status/metrics.md
+++ b/docs/status/metrics.md
@@ -13,6 +13,8 @@
| Trait combinations | 26 | `include/graph/container/traits/` |
| Test count | 4261 | `ctest --preset linux-gcc-debug` (100% pass, 2026-02-19) |
| C++ standard | C++20 | `CMakeLists.txt` line 26 |
+| Line coverage | 95.8% (3300 / 3446) | `docs/status/coverage.md` (2026-02-21) |
+| Function coverage | 92.0% (27193 / 29567) | `docs/status/coverage.md` (2026-02-21) |
| License | BSL-1.0 | `LICENSE` |
| CMake minimum | 3.20 | `CMakeLists.txt` line 1 |
| Consumer target | `graph::graph3` | `cmake/InstallConfig.cmake` (`find_package(graph3)`) |
diff --git a/docs/user-guide/containers.md b/docs/user-guide/containers.md
index 4d67993..9d086e2 100644
--- a/docs/user-guide/containers.md
+++ b/docs/user-guide/containers.md
@@ -137,9 +137,10 @@ container abbreviation, the letter `o`, edge container abbreviation.
| `std::list` | Bidirectional | O(1) insertion/removal anywhere. Edges added to the back. | `l` |
| `std::set` | Bidirectional | Sorted, deduplicated target id | `s` |
| `std::unordered_set` | Forward | Hash-based, O(1) avg lookup, deduplicated target id| `us` |
-| `std::map` | Bidirectional | Sorted by target_id key, deduplicated target id | `em` |
+| `std::map` | Bidirectional | Sorted by target_id key, deduplicated target id | `m` |
+| `std::unordered_map` | Forward | Hash-based, O(1) avg lookup, deduplicated target id | `um` |
-#### Full 26-combination matrix
+#### Full 27-combination matrix
Each trait struct is in `graph::container` and has its own header in
`include/graph/container/traits/`.
@@ -152,7 +153,8 @@ Each trait struct is in `graph::container` and has its own header in
| `vol_graph_traits` | `vector` | `list` | `traits/vol_graph_traits.hpp` |
| `vos_graph_traits` | `vector` | `set` | `traits/vos_graph_traits.hpp` |
| `vous_graph_traits` | `vector` | `unordered_set` | `traits/vous_graph_traits.hpp` |
-| `voem_graph_traits` | `vector` | `map` | `traits/voem_graph_traits.hpp` |
+| `vom_graph_traits` | `vector` | `map` | `traits/vom_graph_traits.hpp` |
+| `voum_graph_traits` | `vector` | `unordered_map` | `traits/voum_graph_traits.hpp` |
| `dov_graph_traits` | `deque` | `vector` | `traits/dov_graph_traits.hpp` |
| `dod_graph_traits` | `deque` | `deque` | `traits/dod_graph_traits.hpp` |
| `dofl_graph_traits` | `deque` | `forward_list` | `traits/dofl_graph_traits.hpp` |
@@ -165,7 +167,7 @@ Each trait struct is in `graph::container` and has its own header in
| `mol_graph_traits` | `map` | `list` | `traits/mol_graph_traits.hpp` |
| `mos_graph_traits` | `map` | `set` | `traits/mos_graph_traits.hpp` |
| `mous_graph_traits` | `map` | `unordered_set` | `traits/mous_graph_traits.hpp` |
-| `moem_graph_traits` | `map` | `map` | `traits/moem_graph_traits.hpp` |
+| `mom_graph_traits` | `map` | `map` | `traits/mom_graph_traits.hpp` |
| `uov_graph_traits` | `unordered_map` | `vector` | `traits/uov_graph_traits.hpp` |
| `uod_graph_traits` | `unordered_map` | `deque` | `traits/uod_graph_traits.hpp` |
| `uofl_graph_traits` | `unordered_map` | `forward_list` | `traits/uofl_graph_traits.hpp` |
@@ -268,7 +270,7 @@ G g;
// All CPOs, views, and algorithms work as normal
```
-> **Tip:** Model your traits struct on one of the 26 built-in traits headers in
+> **Tip:** Model your traits struct on one of the 27 built-in traits headers in
> `include/graph/container/traits/`. The simplest starting point is
> `vov_graph_traits.hpp`.
@@ -362,8 +364,8 @@ for (auto&& u : graph::vertices(g)) {
#include
namespace graph::container {
-template class VContainer = std::vector,
@@ -405,16 +407,13 @@ endpoint). Use `edges_size() / 2` for the unique edge count.
| Parameter | Default | Description |
|-----------|---------|-------------|
-| `VV` | `void` | Vertex value type |
| `EV` | `void` | Edge value type |
+| `VV` | `void` | Vertex value type |
| `GV` | `void` | Graph value type |
| `VId` | `uint32_t` | Vertex ID type (integral) |
| `VContainer` | `std::vector` | Vertex storage container template |
| `Alloc` | `std::allocator` | Allocator |
-> **Note:** The template parameter order is `VV, EV, GV` (vertex-first), which
-> differs from `dynamic_graph` and `compressed_graph` (`EV, VV, GV`).
-
---
## 4. Range-of-Ranges Graphs (No Library Graph Container Required)
@@ -699,7 +698,7 @@ struct), if they are mutable.
| Memory efficiency | Medium | Best (CSR) | Highest overhead | Zero overhead (existing data) |
| Cache locality | Depends on trait | Excellent | Poor (linked-list) | Depends on containers used |
| Multi-partite | No | Yes | No | No |
-| Container flexibility | 26 trait combos | Fixed (CSR) | Configurable random access vertex container | Any forward_range of forward_ranges |
+| Container flexibility | 27 trait combos | Fixed (CSR) | Configurable random access vertex container | Any forward_range of forward_ranges |
**Custom graphs.** See [Section 5 (Custom Graphs)](#5-custom-graphs) for how to use your own
graph data structure with all library views and algorithms by overriding graph CPOs.
diff --git a/include/graph/adj_list/detail/graph_cpo.hpp b/include/graph/adj_list/detail/graph_cpo.hpp
index 96f601d..f0237a1 100644
--- a/include/graph/adj_list/detail/graph_cpo.hpp
+++ b/include/graph/adj_list/detail/graph_cpo.hpp
@@ -2198,11 +2198,19 @@ namespace _cpo_impls {
*
* This provides access to user-defined vertex properties/data stored in the graph.
*
+ * **Return-type preservation guarantee:** The `operator()` returns `decltype(auto)`,
+ * which faithfully preserves the exact return type of the resolved function:
+ * return-by-value (`T`), return-by-reference (`T&`), return-by-const-reference
+ * (`const T&`), and return-by-rvalue-reference (`T&&`) are all forwarded without
+ * decay or copy.
+ *
* @tparam G Graph type
* @tparam U Vertex descriptor type (constrained to be a vertex_descriptor_type)
* @param g Graph container
* @param u Vertex descriptor
- * @return Reference to the vertex value/data
+ * @return Exactly the type returned by the resolved dispatch path (`decltype(auto)`).
+ * By-value, by-reference, by-const-reference, and by-rvalue-reference
+ * are all preserved.
*/
template
[[nodiscard]] constexpr decltype(auto) operator()(G&& g, const U& u) const
@@ -2292,9 +2300,17 @@ namespace _cpo_impls {
* This provides access to user-defined graph-level properties/metadata
* stored in the graph container (e.g., name, creation date, statistics).
*
+ * **Return-type preservation guarantee:** The `operator()` returns `decltype(auto)`,
+ * which faithfully preserves the exact return type of the resolved function:
+ * return-by-value (`T`), return-by-reference (`T&`), return-by-const-reference
+ * (`const T&`), and return-by-rvalue-reference (`T&&`) are all forwarded without
+ * decay or copy.
+ *
* @tparam G Graph type
* @param g Graph container
- * @return Reference to the graph value/properties (or by-value if custom implementation returns by-value)
+ * @return Exactly the type returned by the resolved dispatch path (`decltype(auto)`).
+ * By-value, by-reference, by-const-reference, and by-rvalue-reference
+ * are all preserved.
*/
template
[[nodiscard]] constexpr decltype(auto) operator()(G&& g) const noexcept(_Choice>._No_throw)
diff --git a/include/graph/container/detail/undirected_adjacency_list_api.hpp b/include/graph/container/detail/undirected_adjacency_list_api.hpp
index 822d9bf..0508748 100644
--- a/include/graph/container/detail/undirected_adjacency_list_api.hpp
+++ b/include/graph/container/detail/undirected_adjacency_list_api.hpp
@@ -71,54 +71,54 @@ using const_neighbor_range_t = typename G::const_neighbor_range;
//
// Uniform API: Common functions (accepts graph, vertex and edge)
//
-template class VContainer,
typename Alloc>
-constexpr auto graph_value(const undirected_adjacency_list& g)
- -> const graph_value_t>& {
+constexpr auto graph_value(const undirected_adjacency_list& g)
+ -> const graph_value_t>& {
return user_value(g);
}
//
// API vertex functions
//
-template class VContainer,
typename Alloc>
-constexpr auto vertex_id(const undirected_adjacency_list& g,
- const_vertex_iterator_t> u)
- -> vertex_id_t> {
- return static_cast>>(u -
+constexpr auto vertex_id(const undirected_adjacency_list& g,
+ const_vertex_iterator_t> u)
+ -> vertex_id_t> {
+ return static_cast>>(u -
g.vertices().begin());
}
-template class VContainer,
typename Alloc>
-constexpr auto vertex_value(undirected_adjacency_list& g,
- vertex_iterator_t> u)
- -> vertex_value_t>& {
+constexpr auto vertex_value(undirected_adjacency_list& g,
+ vertex_iterator_t> u)
+ -> vertex_value_t>& {
return user_value(*u);
}
-template class VContainer,
typename Alloc>
-constexpr auto vertex_value(const undirected_adjacency_list& g,
- const_vertex_iterator_t> u)
- -> const vertex_value_t>& {
+constexpr auto vertex_value(const undirected_adjacency_list& g,
+ const_vertex_iterator_t> u)
+ -> const vertex_value_t>& {
return user_value(*u);
}
@@ -128,128 +128,128 @@ constexpr auto vertex_value(const undirected_adjacency_list class VContainer,
typename Alloc>
-constexpr auto edge_id(const undirected_adjacency_list& g,
- const_edge_iterator_t> uv)
- -> edge_id_t