Skip to content

Commit 1a4b76a

Browse files
committed
feat(osint): family-adapter edges + Callcenter DataFusion/Gremlin + aiwar POC
Follow-up to merged #557; rolls in both codex P1 review findings + the operator's 16-family-adapter edge model + the Callcenter slice + an aiwar OSINT POC on the real graph. soa_graph (codex P1 #1 + #2, operator model): - classid IS the class (exact): project_snapshot / nearest_anchor include only rows where classid == domain.classid — a mixed-class board can't leak one domain's nodes/edges into another's view. - 16 x 8-bit family-node adapters: the canonical EdgeBlock is read as 16 family adapters (12 in-family + 4 out-of-family), each non-zero byte -> a FAMILY node by `family & 0xFF`, collision-aware (ambiguous low byte skipped, never mis-routed). Member-by-identity resolution removed -> the >255-member aliasing dissolves (resolution is family-level only). Trade: mixin dependency for extreme render stability + flexibility (E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE). lance-graph-callcenter (the slice): - graph_table (query-lite): GraphSnapshot -> `nodes` + `edges` arrow MemTable TableProviders + register_graph(SessionContext). The DataFusion / SQL / Cypher->SQL path (mirrors transcode::ontology_table). - graph_gremlin (always-on, pure contract types): g(&snap).v().out()/ .in_()/.out_e(label)/.values_kind() — the Gremlin POC = SurrealQL `->edge->` traversal kernel. contract::aiwar + example (the POC the operator asked for): - AiwarClassView (entity category ⇒ family id) + aiwar_node_rows ingest the real AdaWorldAPI/aiwar-neo4j-harvest/data/aiwar_graph.json into OSINT NodeRows; project_snapshot gives a Gotham graph whose family nodes ARE the categories. Example run: 221 entities/326 edges -> 281 nodes (221 members + 60 family hubs) + 481 edges. q2 wires it to the Quadro-2 visual. Tests: contract 703 lib (+5), clippy --all-targets -D warnings clean. callcenter 10 graph tests (--features query, incl. live SQL roundtrip), default build compiles graph_gremlin; new files clippy-clean (pre-existing callcenter query-clippy debt logged TD-CALLCENTER-QUERY-CLIPPY). Board updated (LATEST_STATE, AGENT_LOG, EPIPHANIES, TECH_DEBT, plan). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi
1 parent d865560 commit 1a4b76a

12 files changed

Lines changed: 889 additions & 67 deletions

File tree

.claude/board/AGENT_LOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 2026-06-20 (cont.⁷) — codex roll-up + 16-family-adapter edges + Callcenter DataFusion/Gremlin + aiwar POC
2+
3+
**Main thread (Opus), autoattended.** Follow-up to merged PR #557. Pulled the 2 codex P1 review comments (chatgpt-codex-connector; the CodeRabbit arg-count was fixed pre-merge) and rolled both into this PR per operator. **codex #1** (classid filter): `project_snapshot`/`nearest_anchor` now include only rows where `classid == domain.classid` (a classid IS the class, exact — operator). **codex #2** (ambiguous edge bytes): resolved via the operator's **16×8-bit family-node adapter** model — the `EdgeBlock` is read as 16 family adapters (each byte → a FAMILY by `family & 0xFF`, collision-aware skip), not member-by-identity; the >255-member aliasing dissolves (resolution is family-level only). EPIPHANY `E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE` (mixin-dependency traded for render stability + flexibility).
4+
5+
**Callcenter slice (lance-graph-callcenter):** NEW `graph_table` (`query-lite`) — `GraphSnapshot``nodes`/`edges` arrow MemTable `TableProvider`s + `register_graph(SessionContext)` (the DataFusion/SQL/Cypher→SQL path, mirrors `transcode::ontology_table`); NEW `graph_gremlin` (always-on, pure contract types) — `g(&snap).v().out()/.in_()/.out_e(label)/.values_kind()` Gremlin POC = the SurrealQL `->edge->` traversal kernel. **aiwar POC (contract `aiwar.rs` + example):** `AiwarClassView` (category ⇒ family-id) + `aiwar_node_rows` ingest the REAL `AdaWorldAPI/aiwar-neo4j-harvest/data/aiwar_graph.json` (174 KB) → OSINT NodeRows → `project_snapshot`. Example run: **221 entities/326 edges → 281 nodes (221 members + 60 family hubs) + 481 edges**. (Honest: 60 families because the class view keys off the raw fine-grained `type` field; coarse `N_*`-bucket grouping is a one-line knob — mechanism is correct.)
6+
7+
Tests: contract **703** lib (+5: aiwar ×3, soa_graph ambiguity+mixed-class ×2), clippy `--all-targets -D warnings` clean. Callcenter **10** graph tests (`--features query`, incl. live SQL roundtrip), default build compiles `graph_gremlin`; my two files clippy-clean (pre-existing oxrdf/doc `-D warnings` debt in unrelated modules logged to TECH_DEBT). q2 wires the GraphSnapshot to the Quadro-2 visual. PR opened (codex fixes rolled in).
8+
19
## 2026-06-20 (cont.⁶) — SoA-as-graph domain foundation for q2 (OSINT/Gotham 0x0007 + FMA 0x0008)
210

311
**Main thread (Opus), autoattended.** Operator: "prepare everything so q2 can render nodes/edges + family nodes + HHTL CLAM hop adjacency, neo4j-emulation; OSINT OGAR class is 0x0007; also FMA anatomy 70k as body with bones as stability anchor — rendering is wired in the q2 session, here just the basic domain + SoA-as-graph." Grounded with two parallel Explore agents (q2 wiring + lance-graph ontology/callcenter/polyglot) BEFORE building — consult-don't-guess paid off twice: (a) `graph_render.rs` ALREADY is the Neo4j/Gotham surface (`GraphSnapshot`/`RenderNode`/`RenderEdge`, consumer = q2 cockpit) → reused, not duplicated; (b) `NiblePath::from_guid_prefix` ALREADY is the canonical GUID→path lowering → de-duped symbiont's third copy onto it.

.claude/board/EPIPHANIES.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## 2026-06-20 — E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE — resolving graph edges to FAMILY nodes (16×8-bit family-node adapters) instead of to individual members trades a "mixin dependency" (a referenced family must exist) for two structural wins — extreme render stability (family hubs are fixed anchors, members attach to them, the layout doesn't churn) and the dissolution of the >255-member identity-byte aliasing (resolution is only ever family-level)
2+
3+
**Status:** FINDING (perennial; operator model, shipped `contract::soa_graph` 16-adapter reading + `aiwar` POC, 2026-06-20).
4+
5+
The merged `soa_graph` (PR #557) resolved the 12 in-family edge slots to individual sibling members by `identity & 0xFF`. Codex P1 #2 caught the flaw: a family with >255 members aliases on the low byte, so "first match" renders an edge to the *wrong* member by row order. The operator's fix is not a wider member index — it is a **reinterpretation**: read the canonical 16-byte `EdgeBlock` as **16 family-node adapter slots**, every byte resolving to a FAMILY (`family & 0xFF`), never an individual member.
6+
7+
Why this is the right move, not just a bug patch:
8+
1. **The aliasing dissolves at the source.** There is no longer any member-level byte resolution — edges land on family nodes. The only remaining ambiguity is two families sharing a low byte (>256 families), handled by a collision-aware map that skips the ambiguous byte (never a wrong edge). 256 families "covers pretty much everything" for a POC; the prefix/HHTL route is the >256 escalation.
9+
2. **Render stability is structural.** Family nodes are fixed hubs; members attach to them (member-of) and adjacency is member→family. A force-directed layout anchored on a small stable set of family hubs does not churn frame-to-frame — the operator's "extreme render stability." This is the same stability `E-ANCHOR-IS-A-HEAD-FIELD` gives the FMA skeleton (bones = anchor families); the two epiphanies are the static (anchor) and dynamic (edge-resolution) faces of one principle: **structure lives on families, in the head.**
10+
3. **Flexibility + the one cost.** A node mixes in up to 16 family adjacencies (huge flexibility, any-to-any within 256). The named limitation is **mixin dependency**: a referenced family must exist or the slot is a dangling adapter (skipped). That is the honest trade — and it is cheap, because a missing family is a render no-op, not a corruption.
11+
12+
The general rule for graph edges on this substrate: **resolve to the stable grouping (family), not the volatile leaf (member)** — unless a richer flavor (8×16-bit, 32×4 residue, member→member second-hop) is measured to be needed. Cross-ref: `E-ANCHOR-IS-A-HEAD-FIELD-NOT-A-VALUE-TYPE` (the static dual), `E-GUID-IS-THE-GRAPH`, the operator's deferred helix-basin-anchor (CLAM ⇄ Louvain turbovec edge residue) as the eventual richer flavor; `aiwar.rs` (the POC: 221 aiwar entities → 60 category family hubs).
13+
14+
---
15+
116
## 2026-06-20 — E-ANCHOR-IS-A-HEAD-FIELD-NOT-A-VALUE-TYPE — graph STRUCTURE (domain, family grouping, hierarchy, stability anchors, adjacency) must key off the 32-byte HEAD (classid / family / HHTL path), never the value slab; only then does the whole neo4j/Gotham view — and "FMA bones as stability anchor" — stay zero-value-decode at memory-scan speed
217

318
**Status:** FINDING (perennial; shipped `contract::soa_graph` + `NiblePath::family_hop_count`, 2026-06-20).

.claude/board/LATEST_STATE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
---
1818

19+
> **2026-06-20 — IN PR (`claude/jirak-math-theorems-harvest-rfii13`)** — **codex roll-up + 16-family-adapter edges + Callcenter DataFusion/Gremlin + aiwar POC.** Follow-up to merged #557. (1) Both codex P1 fixes rolled in: classid filter (`project_snapshot`/`nearest_anchor` only project `classid == domain.classid` rows) + the operator's **16×8-bit family-node adapter** edge model — the `EdgeBlock` reads as 16 family adapters (each byte → a FAMILY by `family & 0xFF`, collision-aware skip), dissolving the >255-member aliasing; member-by-identity resolution removed (`E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE`). (2) `lance-graph-callcenter`: NEW `graph_table` (`query-lite`, `GraphSnapshot` → `nodes`/`edges` arrow MemTable `TableProvider`s + `register_graph(SessionContext)`) + NEW `graph_gremlin` (always-on Gremlin/SurrealQL traversal kernel). (3) `contract::aiwar` + example: `AiwarClassView` (category ⇒ family) + `aiwar_node_rows` ingest the real `aiwar-neo4j-harvest/data/aiwar_graph.json` (221 entities → 281 nodes / 60 family hubs / 481 edges). Contract 703 lib + callcenter 10 graph tests green; contract clippy `--all-targets -D warnings` clean. q2 wires the GraphSnapshot → Quadro-2 visual. Refs: AGENT_LOG 2026-06-20 (cont.⁷), EPIPHANIES `E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE`, TECH_DEBT `TD-CALLCENTER-QUERY-CLIPPY`.
20+
>
1921
> **2026-06-20 — branch work (`claude/jirak-math-theorems-harvest-rfii13`)** — **SoA-as-graph domain foundation for the OSINT/Gotham + FMA consumers (q2 renders the pixels).** New zero-dep `contract::soa_graph`: `project_snapshot(&[NodeRow], &DomainSpec) -> graph_render::GraphSnapshot` projects the canonical 32-byte head (NodeGuid + EdgeBlock) into the EXISTING Gotham/neo4j surface (`graph_render` — reused, not duplicated) — family nodes (by u24 `family`), member/in-family/out-of-family edges, all **zero value decode**. `nearest_anchor` ranks nodes to their nearest stability-anchor family by the new `NiblePath::family_hop_count` (CLAM tree distance). Two domains registered: `OSINT_GOTHAM` (classid **`0x0007`**) + `FMA_ANATOMY` (**`0x0008`**, bones = anchor families) in `BUILTIN_READ_MODES` (`ReadMode::OSINT` Cognitive/CoarseOnly hot; `ReadMode::FMA` Compressed/CoarseOnly cold). Anchor-ness is a HEAD field (`family`), never a value type — so "FMA bones as stability anchor" stays head-only (`E-ANCHOR-IS-A-HEAD-FIELD-NOT-A-VALUE-TYPE`). De-duped the GUID→NiblePath lowering: symbiont's `hhtl_path_of` now delegates to canonical `from_guid_prefix` (third copy collapsed). 698 contract + 12 symbiont tests green, clippy clean. **Deferred (named):** q2 rendering (q2 session), Callcenter DataFusion/gremlin POC, OntologyRegistry ClassView labels. Refs: AGENT_LOG 2026-06-20 (cont.⁶), EPIPHANIES `E-ANCHOR-IS-A-HEAD-FIELD-NOT-A-VALUE-TYPE`.
2022
>
2123
> **2026-06-20 — branch work (`claude/happy-hamilton-0azlw4`)** — **UNICHARSET `other_case` transcoded + byte-parity proven (E-CPP-PARITY-5), the fifth leaf.** `UniCharSet` now parses the case-pair id (the token right after the script) into `other_cases: Vec<i32>`, applying the load-time clamp (`unicharset.cpp:901`: a value `>= size`, incl. the absent default, folds to the id itself). Exposes `get_other_case` + `dump_other_case`, mirroring `unicharset.h:703` (out-of-range id → `INVALID_UNICHAR_ID` -1). **Byte-identical 112/112** on real `eng.lstm-unicharset` vs tesseract's own `get_other_case` (self-validating oracle, `other_case` mode; 60/112 self, 52 real pairs, e.g. `C`→`c`). Last field cleanly reachable by token-offset; direction/mirror/bbox need the multi-tier parser (next, larger leaf). Additive, zero-dep; +4 contract tests (23 unicharset total), clippy `-D warnings` + fmt clean; reproducible via `examples/unicharset_dump.rs other_case`. Consumed by `tesseract-core::CharSet::get_other_case` (+1 boundary test, 6/6). No Core gap. EPIPHANIES `E-CPP-PARITY-5`.

.claude/board/TECH_DEBT.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2890,3 +2890,6 @@ after hydration (the OGAR/hydrator seed step does this). Persisting the pairs
28902890
the Paid state. Low risk: dedup itself survives replay (the deduped
28912891
`entity_type` is baked into each persisted `schema_ptr`); only the path
28922892
bijection needs re-seeding. Pair: D-IDENTITY-4.
2893+
2894+
## TD-CALLCENTER-QUERY-CLIPPY — 2026-06-20 — OPEN
2895+
**Owed:** `cargo clippy -p lance-graph-callcenter --features query --lib --tests -- -D warnings` fails on PRE-EXISTING debt unrelated to the graph-adapter work: `oxrdf::Subject` deprecation (`src/edge.rs`, use `NamedOrBlankNode`), doc-list overindent + misc in `savant_reasoners.rs` / `vsa_udfs.rs` / `ttl_parse.rs` / `transcode/zerocopy.rs` / `hydrators/owl.rs`. Surfaced (not caused) by the 16-family-adapter PR, whose own files (`graph_table.rs`, `graph_gremlin.rs`) are clippy-clean (verified: zero errors originate in them). **Why deferred:** out of scope — fixing the oxrdf deprecation + doc lints across 6 unrelated modules is its own sweep; this PR is the OSINT graph adapter. **Pay when:** a callcenter-clippy-hardening pass, or opportunistically when those modules are next touched.

.claude/plans/unified-soa-rubikon-integration-v1.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ No copies, no per-subsystem mirror (R1 "one SoA never transformed").
5454
16384 nodes / 32768 edges from 512 KiB of heads, 7680 KiB of value slabs COLD;
5555
zero-value-decode proven by the `0xFF`-poison falsifiable probe. `SymbiontBoard`
5656
now materialises the contract's `edge_block_at`/`hhtl_path_at` key facets.
57+
-**OSINT family-adapter edges + Callcenter DataFusion/Gremlin + aiwar POC**
58+
(follow-up to #557, codex P1 fixes rolled in) — `soa_graph` now reads the
59+
`EdgeBlock` as **16×8-bit family-node adapters** (edges → families, collision-
60+
aware; dissolves >255-member aliasing) + classid-filtered projection;
61+
`lance-graph-callcenter::graph_table` (DataFusion `nodes`/`edges` MemTables) +
62+
`graph_gremlin` (Gremlin/SurrealQL traversal); `contract::aiwar` ingests the
63+
real `aiwar-neo4j-harvest` graph (221 entities → 60 family hubs). q2 wires the
64+
GraphSnapshot → Quadro-2 visual. `E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE`.
5765
-**SoA-as-graph domain foundation for q2 (OSINT/Gotham + FMA)**
5866
`contract::soa_graph` projects the head into the EXISTING `graph_render`
5967
Gotham/neo4j surface (`GraphSnapshot`): family nodes (u24 `family`),
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
//! `graph_gremlin` — a minimal Gremlin-style traversal over a [`GraphSnapshot`].
2+
//!
3+
//! The pure-Rust contrast to the DataFusion node/edge tables ([`crate::graph_table`],
4+
//! `query-lite`): `g(&snap).v(&["id"]).out().to_vec()` walks the adjacency in the
5+
//! snapshot with **zero SQL, zero DataFusion** — just the `source → target` edge
6+
//! list. A "very basic Gremlin POC": `V` / `out` / `in_` / `out_e(label)` /
7+
//! `in_e(label)` / `values_kind` / `to_vec` / `count`.
8+
//!
9+
//! SurrealQL graph traversal lowers to the SAME steps — `->edge->` ≈ [`out`],
10+
//! `<-edge<-` ≈ [`in_`], `->edge(WHERE ...)->` ≈ [`out_e`] — so this doubles as
11+
//! the SurrealQL traversal kernel over the family-adapter graph. Both consume the
12+
//! `GraphSnapshot` the SoA projector ([`lance_graph_contract::soa_graph`])
13+
//! produces from the 32-byte node head; the family nodes are the stable hubs the
14+
//! traversal hops through.
15+
//!
16+
//! [`out`]: Traversal::out
17+
//! [`in_`]: Traversal::in_
18+
//! [`out_e`]: Traversal::out_e
19+
20+
use lance_graph_contract::graph_render::GraphSnapshot;
21+
use std::collections::HashSet;
22+
23+
/// The Gremlin `g` — a traversal source bound to one graph snapshot.
24+
pub struct GraphTraversalSource<'a> {
25+
snap: &'a GraphSnapshot,
26+
}
27+
28+
/// `g(&snap)` — open a traversal over the snapshot (the Gremlin `g`).
29+
pub fn g(snap: &GraphSnapshot) -> GraphTraversalSource<'_> {
30+
GraphTraversalSource { snap }
31+
}
32+
33+
impl<'a> GraphTraversalSource<'a> {
34+
/// `g.V(ids)` — seed the traversal at the given vertex ids. An empty slice is
35+
/// `g.V()` (all vertices).
36+
pub fn v(&self, ids: &[&str]) -> Traversal<'a> {
37+
let current: Vec<String> = if ids.is_empty() {
38+
self.snap.nodes.iter().map(|n| n.id.clone()).collect()
39+
} else {
40+
ids.iter().map(|s| s.to_string()).collect()
41+
};
42+
Traversal {
43+
snap: self.snap,
44+
current,
45+
}
46+
}
47+
}
48+
49+
/// An in-flight traversal: the multiset of vertex ids currently held, plus the
50+
/// snapshot to hop over. Steps consume `self` and return `Self` (Gremlin fluent).
51+
pub struct Traversal<'a> {
52+
snap: &'a GraphSnapshot,
53+
current: Vec<String>,
54+
}
55+
56+
impl<'a> Traversal<'a> {
57+
/// `out()` — follow outgoing edges (`source ∈ current → target`), any label.
58+
#[must_use]
59+
pub fn out(mut self) -> Self {
60+
self.step(None, true);
61+
self
62+
}
63+
64+
/// `out(label)` — outgoing edges whose label equals `label`.
65+
#[must_use]
66+
pub fn out_e(mut self, label: &str) -> Self {
67+
self.step(Some(label), true);
68+
self
69+
}
70+
71+
/// `in()` — follow incoming edges (`target ∈ current → source`), any label.
72+
#[must_use]
73+
pub fn in_(mut self) -> Self {
74+
self.step(None, false);
75+
self
76+
}
77+
78+
/// `in(label)` — incoming edges whose label equals `label`.
79+
#[must_use]
80+
pub fn in_e(mut self, label: &str) -> Self {
81+
self.step(Some(label), false);
82+
self
83+
}
84+
85+
fn step(&mut self, label: Option<&str>, outgoing: bool) {
86+
let cur: HashSet<&str> = self.current.iter().map(String::as_str).collect();
87+
let mut next: Vec<String> = Vec::new();
88+
let mut seen: HashSet<String> = HashSet::new();
89+
for e in &self.snap.edges {
90+
if let Some(l) = label {
91+
if e.label != l {
92+
continue;
93+
}
94+
}
95+
let (from, to) = if outgoing {
96+
(&e.source, &e.target)
97+
} else {
98+
(&e.target, &e.source)
99+
};
100+
if cur.contains(from.as_str()) && seen.insert(to.clone()) {
101+
next.push(to.clone());
102+
}
103+
}
104+
self.current = next;
105+
}
106+
107+
/// Terminal `values("kind")` — project the `kind` of each reached vertex
108+
/// (skips ids that are not present as nodes, e.g. a dangling adapter target).
109+
#[must_use]
110+
pub fn values_kind(&self) -> Vec<String> {
111+
self.current
112+
.iter()
113+
.filter_map(|id| {
114+
self.snap
115+
.nodes
116+
.iter()
117+
.find(|n| &n.id == id)
118+
.map(|n| n.kind.clone())
119+
})
120+
.collect()
121+
}
122+
123+
/// Terminal `toList()` — the vertex ids currently reached.
124+
#[must_use]
125+
pub fn to_vec(self) -> Vec<String> {
126+
self.current
127+
}
128+
129+
/// Terminal `count()` — number of vertices reached.
130+
#[must_use]
131+
pub fn count(self) -> usize {
132+
self.current.len()
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::*;
139+
use lance_graph_contract::graph_render::{RenderEdge, RenderNode};
140+
141+
fn node(id: &str, kind: &str) -> RenderNode {
142+
RenderNode {
143+
id: id.to_string(),
144+
label: id.to_string(),
145+
kind: kind.to_string(),
146+
confidence: 1.0,
147+
props: vec![],
148+
}
149+
}
150+
fn edge(source: &str, target: &str, label: &str) -> RenderEdge {
151+
RenderEdge {
152+
source: source.to_string(),
153+
target: target.to_string(),
154+
label: label.to_string(),
155+
frequency: 1.0,
156+
confidence: 1.0,
157+
inferred: false,
158+
}
159+
}
160+
161+
fn sample() -> GraphSnapshot {
162+
// A -knows-> B -knows-> C ; A -member-of-> family:00000a
163+
GraphSnapshot {
164+
nodes: vec![
165+
node("A", "Person"),
166+
node("B", "Person"),
167+
node("C", "Person"),
168+
node("family:00000a", "Family"),
169+
],
170+
edges: vec![
171+
edge("A", "B", "knows"),
172+
edge("B", "C", "knows"),
173+
edge("A", "family:00000a", "member-of"),
174+
],
175+
inferences: vec![],
176+
contradictions: vec![],
177+
timestamp: 0,
178+
}
179+
}
180+
181+
#[test]
182+
fn out_follows_outgoing_edges() {
183+
let s = sample();
184+
assert_eq!(g(&s).v(&["A"]).out().to_vec(), vec!["B", "family:00000a"]);
185+
}
186+
187+
#[test]
188+
fn in_follows_incoming_edges() {
189+
let s = sample();
190+
assert_eq!(g(&s).v(&["B"]).in_().to_vec(), vec!["A".to_string()]);
191+
}
192+
193+
#[test]
194+
fn out_e_filters_by_label() {
195+
let s = sample();
196+
// g.V("A").out("knows") = [B]; out("member-of") = [family:00000a]
197+
assert_eq!(g(&s).v(&["A"]).out_e("knows").to_vec(), vec!["B"]);
198+
assert_eq!(
199+
g(&s).v(&["A"]).out_e("member-of").to_vec(),
200+
vec!["family:00000a"]
201+
);
202+
}
203+
204+
#[test]
205+
fn two_hop_traversal() {
206+
let s = sample();
207+
// g.V("A").out("knows").out("knows") = [C]
208+
assert_eq!(
209+
g(&s).v(&["A"]).out_e("knows").out_e("knows").to_vec(),
210+
vec!["C"]
211+
);
212+
}
213+
214+
#[test]
215+
fn values_kind_projects_node_property() {
216+
let s = sample();
217+
// A's "member-of" neighbour is the family hub → kind "Family".
218+
assert_eq!(g(&s).v(&["A"]).out_e("member-of").values_kind(), vec!["Family"]);
219+
}
220+
221+
#[test]
222+
fn unknown_label_yields_empty() {
223+
let s = sample();
224+
assert_eq!(g(&s).v(&["A"]).out_e("nope").count(), 0);
225+
}
226+
227+
#[test]
228+
fn v_with_no_seed_is_all_vertices() {
229+
let s = sample();
230+
assert_eq!(g(&s).v(&[]).count(), 4);
231+
}
232+
}

0 commit comments

Comments
 (0)