diff --git a/crates/bobbin-sim/tests/warming_buffer.rs b/crates/bobbin-sim/tests/warming_buffer.rs index 35339e0..8bf2a01 100644 --- a/crates/bobbin-sim/tests/warming_buffer.rs +++ b/crates/bobbin-sim/tests/warming_buffer.rs @@ -175,8 +175,8 @@ fn warming_to_ready_promote_drains_residual_via_parallel_slingshot_wave() { report.resolver_misses, ); assert_eq!( - report.edge_count, stars + 1, - "every drained star plus the live promoter repo should land in the index, got {} for {stars} stars", + report.edge_count, stars * 2 + 1, + "each drained star contributes a primary edge plus a sh.tangled.feed.star.by mirror edge; promoter repo adds one. got {} for {stars} stars", report.edge_count, ); } diff --git a/crates/types/src/edges.rs b/crates/types/src/edges.rs index 45b25e5..62981a1 100644 --- a/crates/types/src/edges.rs +++ b/crates/types/src/edges.rs @@ -158,6 +158,11 @@ impl Record { } pub fn extract_edges(&self, source: &AtUri) -> Result, ExtractError> { + let primary = self.primary_edges(source)?; + Ok(append_mirror_edges(primary, source)) + } + + fn primary_edges(&self, source: &AtUri) -> Result, ExtractError> { match self { Self::Star(r) => star_edges(source, r), Self::Reaction(r) => reaction_edges(source, r), @@ -182,7 +187,8 @@ impl Record { Self::Repo(_) => Ok(owner_self_edges("sh.tangled.repo", source)), Self::Spindle(_) => Ok(owner_self_edges("sh.tangled.spindle", source)), Self::TangledString(_) => Ok(owner_self_edges("sh.tangled.string", source)), - Self::Profile(_) | Self::Vouch(_) => Ok(Vec::new()), + Self::Vouch(r) => vouch_edges(source, r), + Self::Profile(_) => Ok(Vec::new()), } } } @@ -222,6 +228,55 @@ fn one_edge( }] } +const MIRROR_KINDS: &[(&str, &str)] = &[ + ("sh.tangled.feed.star", "sh.tangled.feed.star.by"), + ("sh.tangled.feed.reaction", "sh.tangled.feed.reaction.by"), + ("sh.tangled.graph.follow", "sh.tangled.graph.follow.by"), + ("sh.tangled.graph.vouch", "sh.tangled.graph.vouch.by"), + ("sh.tangled.git.refUpdate", "sh.tangled.git.refUpdate.by"), + ("sh.tangled.knot.member", "sh.tangled.knot.member.by"), + ("sh.tangled.label.op", "sh.tangled.label.op.by"), + ("sh.tangled.pipeline", "sh.tangled.pipeline.by"), + ("sh.tangled.pipeline.status", "sh.tangled.pipeline.status.by"), + ("sh.tangled.repo.artifact", "sh.tangled.repo.artifact.by"), + ("sh.tangled.repo.collaborator", "sh.tangled.repo.collaborator.by"), + ("sh.tangled.repo.issue", "sh.tangled.repo.issue.by"), + ("sh.tangled.repo.issue.comment", "sh.tangled.repo.issue.comment.by"), + ("sh.tangled.repo.issue.state", "sh.tangled.repo.issue.state.by"), + ("sh.tangled.repo.pull", "sh.tangled.repo.pull.by"), + ("sh.tangled.repo.pull.comment", "sh.tangled.repo.pull.comment.by"), + ("sh.tangled.repo.pull.status", "sh.tangled.repo.pull.status.by"), + ("sh.tangled.spindle.member", "sh.tangled.spindle.member.by"), +]; + +fn mirror_kind_for(kind: &str) -> Option<&'static str> { + MIRROR_KINDS + .iter() + .find(|(primary, _)| *primary == kind) + .map(|(_, mirror)| *mirror) +} + +fn append_mirror_edges(primary: Vec, source: &AtUri) -> Vec { + let Some(author) = crate::ids::did_from_aturi(source.as_ref()) else { + return primary; + }; + let Ok(author_uri) = did_to_aturi(&author) else { + return primary; + }; + let mirrors: Vec = primary + .iter() + .filter_map(|edge| { + let mirror_nsid = mirror_kind_for(edge.kind.as_ref())?; + (edge.subject != author_uri).then(|| Edge { + kind: nsid_static(mirror_nsid), + subject: author_uri.clone(), + source: edge.source.clone(), + }) + }) + .collect(); + primary.into_iter().chain(mirrors).collect() +} + fn star_edges( source: &AtUri, record: &Star, @@ -385,6 +440,23 @@ fn pull_status_edges( )) } +fn vouch_edges( + source: &AtUri, + _record: &Vouch, +) -> Result, ExtractError> { + let Some(rkey) = source.rkey() else { + return Ok(Vec::new()); + }; + let Ok(vouchee) = Did::::new_owned(rkey.as_ref()) else { + return Ok(Vec::new()); + }; + Ok(one_edge( + "sh.tangled.graph.vouch", + did_to_aturi(&vouchee)?, + source, + )) +} + fn spindle_member_edges( source: &AtUri, record: &SpindleMemberRecord, @@ -435,6 +507,11 @@ mod tests { } fn extract(collection: &'static str, source: &str, body: serde_json::Value) -> Vec { + let parsed = Record::from_json_value(&nsid(collection), body).expect("parse"); + parsed.primary_edges(&at(source)).expect("extract") + } + + fn extract_all(collection: &'static str, source: &str, body: serde_json::Value) -> Vec { let parsed = Record::from_json_value(&nsid(collection), body).expect("parse"); parsed.extract_edges(&at(source)).expect("extract") }