Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

bobbin: match issue/pull comments as sh.tangled.feed.comment

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
date (May 29, 2026, 2:50 PM +0300) commit 26391166 parent 30707ac8 change-id kxzzukmw
+312 -323
+1 -1
bobbin/crates/edge-index/tests/bucket_scaling.rs
··· 33 33 } 34 34 35 35 thread_local! { 36 - static COMPARES: Cell<u128> = Cell::new(0); 36 + static COMPARES: Cell<u128> = const { Cell::new(0) }; 37 37 } 38 38 39 39 #[derive(PartialEq, Eq)]
+16 -8
bobbin/crates/ingest/benches/json_decode.rs
··· 81 81 "record": { 82 82 "$type": "sh.tangled.feed.star", 83 83 "createdAt": "2026-05-01T00:00:00Z", 84 - "subject": "at://did:plc:abalone/sh.tangled.repo/3lq2zk5wq0000" 84 + "subject": { 85 + "$type": "sh.tangled.feed.star#repo", 86 + "did": "did:plc:limpet" 87 + } 85 88 } 86 89 } 87 90 }"# ··· 131 134 "createdAt": "2026-05-01T00:00:00Z", 132 135 "title": "ingest: single-pass JSON decode follow-up corpus entry", 133 136 "body": "Long body to give the bench a realistic decode cost. Repeats: blahhhhh meow meow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 134 - "repo": "at://did:plc:limpet", 135 - "repoDid": "did:plc:limpet", 137 + "repo": "did:plc:limpet", 136 138 "mentions": [ 137 139 "did:plc:nel", 138 140 "did:plc:olaren", ··· 155 157 "live": false, 156 158 "did": "did:plc:lyna", 157 159 "rev": "3lq2zk5wqsh2n", 158 - "collection": "sh.tangled.repo.pull.comment", 160 + "collection": "sh.tangled.feed.comment", 159 161 "rkey": "3lq2zk5wq0200", 160 162 "action": "create", 161 163 "record": { 162 - "$type": "sh.tangled.repo.pull.comment", 164 + "$type": "sh.tangled.feed.comment", 163 165 "createdAt": "2026-05-01T00:00:00Z", 164 - "body": "lgtm i thinks!!!! but please verify the cursor invariant under buffered(N) before landing. :3", 165 - "pull": "at://did:plc:limpet/sh.tangled.repo.pull/3lq2zk5wq0098", 166 - "owner": "did:plc:lyna" 166 + "body": { 167 + "$type": "sh.tangled.markup.markdown", 168 + "text": "lgtm i thinks!!!! but please verify the cursor invariant under buffered(N) before landing. :3" 169 + }, 170 + "subject": { 171 + "uri": "at://did:plc:limpet/sh.tangled.repo.pull/3lq2zk5wq0098", 172 + "cid": "bafkqaaa" 173 + }, 174 + "pullRoundIdx": 0 167 175 } 168 176 } 169 177 }"#
+86 -3
bobbin/crates/resolver/src/legacy_upgrade.rs
··· 1 + use alloc::collections::BTreeMap; 2 + use bobbin_types::com_atproto::repo::strong_ref::StrongRef; 1 3 use bobbin_types::edges::{ExtractError, Record}; 2 4 use bobbin_types::legacy::{ 3 - LegacyCollaborator, LegacyIssue, LegacyKnotMember, LegacyPublicKey, LegacyPull, LegacyRecord, 5 + LEGACY_COMMENT_SENTINEL_CID, LegacyCollaborator, LegacyIssue, LegacyIssueComment, 6 + LegacyKnotMember, LegacyPublicKey, LegacyPull, LegacyPullComment, LegacyRecord, 4 7 LegacyRefUpdate, LegacyRepo, LegacySource, LegacyStar, LegacyTarget, 5 8 }; 9 + use bobbin_types::sh_tangled::feed::comment::Comment as FeedComment; 6 10 use bobbin_types::sh_tangled::feed::star::{Repo as StarRepo, Star, StarString, StarSubject}; 7 11 use bobbin_types::sh_tangled::git::ref_update::RefUpdate; 8 12 use bobbin_types::sh_tangled::knot::member::Member as KnotMember; 13 + use bobbin_types::sh_tangled::markup::markdown::Markdown; 9 14 use bobbin_types::sh_tangled::public_key::PublicKey; 10 15 use bobbin_types::sh_tangled::repo::Repo; 11 16 use bobbin_types::sh_tangled::repo::collaborator::Collaborator; 12 17 use bobbin_types::sh_tangled::repo::issue::Issue; 13 18 use bobbin_types::sh_tangled::repo::pull::{Pull, Round, Source, Target}; 19 + use jacquard_common::deps::smol_str::SmolStr; 14 20 use jacquard_common::types::did::Did; 15 21 use jacquard_common::types::nsid::Nsid; 16 - use jacquard_common::types::string::AtUri; 22 + use jacquard_common::types::string::{AtUri, AtprotoStr, Cid}; 23 + use jacquard_common::types::value::{Array, Data}; 17 24 use jacquard_common::{BosStr, DefaultStr}; 18 25 19 26 use crate::normalize::{is_repo_at_uri, resolve_repo_uri}; ··· 201 208 202 209 fn serialize_canon_variant(record: &Record) -> Result<alloc::vec::Vec<u8>, serde_json::Error> { 203 210 match record { 211 + Record::FeedComment(r) => serde_json::to_vec(r), 204 212 Record::Issue(r) => serde_json::to_vec(r), 205 213 Record::Pull(r) => serde_json::to_vec(r), 206 214 Record::Collaborator(r) => serde_json::to_vec(r), ··· 210 218 Record::Repo(r) => serde_json::to_vec(r), 211 219 Record::KnotMember(r) => serde_json::to_vec(r), 212 220 _ => unreachable!( 213 - "upgrade only produces Issue/Pull/Collaborator/RefUpdate/Star/PublicKey/Repo/KnotMember" 221 + "upgrade only produces FeedComment/Issue/Pull/Collaborator/RefUpdate/Star/PublicKey/Repo/KnotMember" 214 222 ), 215 223 } 216 224 } ··· 218 226 pub async fn upgrade(legacy: LegacyRecord, resolver: &RepoIdResolver) -> Option<Record> { 219 227 match legacy { 220 228 LegacyRecord::Issue(l) => upgrade_issue(l, resolver).await.map(Record::Issue), 229 + LegacyRecord::IssueComment(l) => Some(Record::FeedComment(upgrade_issue_comment(l))), 221 230 LegacyRecord::Pull(l) => upgrade_pull(l, resolver).await.map(Record::Pull), 231 + LegacyRecord::PullComment(l) => Some(Record::FeedComment(upgrade_pull_comment(l))), 222 232 LegacyRecord::Collaborator(l) => upgrade_collaborator(l, resolver) 223 233 .await 224 234 .map(Record::Collaborator), ··· 227 237 LegacyRecord::PublicKey(l) => Some(Record::PublicKey(upgrade_public_key(l))), 228 238 LegacyRecord::Repo(l) => Some(Record::Repo(upgrade_repo(l))), 229 239 LegacyRecord::KnotMember(l) => Some(Record::KnotMember(upgrade_knot_member(l))), 240 + } 241 + } 242 + 243 + fn sentinel_strong_ref(uri: AtUri<DefaultStr>) -> StrongRef<DefaultStr> { 244 + let cid = Cid::<DefaultStr>::new_owned(LEGACY_COMMENT_SENTINEL_CID.as_bytes()) 245 + .expect("LEGACY_COMMENT_SENTINEL_CID is a valid CID literal"); 246 + StrongRef { 247 + uri, 248 + cid, 249 + extra_data: None, 250 + } 251 + } 252 + 253 + fn legacy_body_markdown(text: DefaultStr) -> Markdown<DefaultStr> { 254 + Markdown { 255 + blobs: None, 256 + original: None, 257 + text, 258 + extra_data: None, 259 + } 260 + } 261 + 262 + fn upgrade_issue_comment(l: LegacyIssueComment<DefaultStr>) -> FeedComment<DefaultStr> { 263 + FeedComment { 264 + body: legacy_body_markdown(l.body), 265 + created_at: l.created_at, 266 + pull_round_idx: None, 267 + reply_to: l.reply_to.map(sentinel_strong_ref), 268 + subject: sentinel_strong_ref(l.issue), 269 + extra_data: legacy_comment_extras(l.extra_data, l.mentions, l.references), 270 + } 271 + } 272 + 273 + fn upgrade_pull_comment(l: LegacyPullComment<DefaultStr>) -> FeedComment<DefaultStr> { 274 + FeedComment { 275 + body: legacy_body_markdown(l.body), 276 + created_at: l.created_at, 277 + pull_round_idx: None, 278 + reply_to: None, 279 + subject: sentinel_strong_ref(l.pull), 280 + extra_data: legacy_comment_extras(l.extra_data, l.mentions, l.references), 281 + } 282 + } 283 + 284 + fn legacy_comment_extras<S: BosStr>( 285 + base: Option<BTreeMap<SmolStr, Data<S>>>, 286 + mentions: Option<Vec<Did<S>>>, 287 + references: Option<Vec<AtUri<S>>>, 288 + ) -> Option<BTreeMap<SmolStr, Data<S>>> { 289 + let mention_entry = mentions.filter(|v| !v.is_empty()).map(|items| { 290 + let arr = items 291 + .into_iter() 292 + .map(|d| Data::String(AtprotoStr::Did(d))) 293 + .collect(); 294 + (SmolStr::new_static("mentions"), Data::Array(Array(arr))) 295 + }); 296 + let reference_entry = references.filter(|v| !v.is_empty()).map(|items| { 297 + let arr = items 298 + .into_iter() 299 + .map(|u| Data::String(AtprotoStr::AtUri(u))) 300 + .collect(); 301 + (SmolStr::new_static("references"), Data::Array(Array(arr))) 302 + }); 303 + let combined: BTreeMap<SmolStr, Data<S>> = base 304 + .into_iter() 305 + .flatten() 306 + .chain(mention_entry) 307 + .chain(reference_entry) 308 + .collect(); 309 + if combined.is_empty() { 310 + None 311 + } else { 312 + Some(combined) 230 313 } 231 314 } 232 315
+2 -4
bobbin/crates/resolver/src/normalize.rs
··· 105 105 } 106 106 107 107 use bobbin_types::sh_tangled::actor::profile::Profile; 108 + use bobbin_types::sh_tangled::feed::comment::Comment as FeedComment; 108 109 use bobbin_types::sh_tangled::feed::reaction::Reaction; 109 110 use bobbin_types::sh_tangled::feed::star::Star; 110 111 use bobbin_types::sh_tangled::git::ref_update::RefUpdate; ··· 119 120 use bobbin_types::sh_tangled::repo::Repo; 120 121 use bobbin_types::sh_tangled::repo::collaborator::Collaborator; 121 122 use bobbin_types::sh_tangled::repo::issue::Issue; 122 - use bobbin_types::sh_tangled::repo::issue::comment::Comment as IssueComment; 123 123 use bobbin_types::sh_tangled::repo::issue::state::State as IssueState; 124 124 use bobbin_types::sh_tangled::repo::pull::Pull; 125 - use bobbin_types::sh_tangled::repo::pull::comment::Comment as PullComment; 126 125 use bobbin_types::sh_tangled::repo::pull::status::Status as PullStatus; 127 126 use bobbin_types::sh_tangled::spindle::Spindle; 128 127 use bobbin_types::sh_tangled::spindle::member::Member as SpindleMember; ··· 130 129 131 130 identity_normalize!( 132 131 Profile<DefaultStr>, 132 + FeedComment<DefaultStr>, 133 133 Reaction<DefaultStr>, 134 134 Star<DefaultStr>, 135 135 RefUpdate<DefaultStr>, ··· 144 144 Repo<DefaultStr>, 145 145 Collaborator<DefaultStr>, 146 146 Issue<DefaultStr>, 147 - IssueComment<DefaultStr>, 148 147 IssueState<DefaultStr>, 149 148 Pull<DefaultStr>, 150 - PullComment<DefaultStr>, 151 149 PullStatus<DefaultStr>, 152 150 Spindle<DefaultStr>, 153 151 SpindleMember<DefaultStr>,
-62
bobbin/crates/types/lexicons/repo/issue/listComments.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.repo.issue.listComments", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "parameters": { 8 - "type": "params", 9 - "required": ["subject"], 10 - "properties": { 11 - "subject": { 12 - "type": "string", 13 - "format": "at-uri", 14 - "description": "Issue AT-URI whose comments to list." 15 - }, 16 - "cursor": { 17 - "type": "string", 18 - "description": "Pagination cursor" 19 - }, 20 - "limit": { 21 - "type": "integer", 22 - "minimum": 1, 23 - "maximum": 1000, 24 - "default": 50 25 - }, 26 - "order": { 27 - "type": "string", 28 - "knownValues": ["asc", "desc"], 29 - "default": "desc", 30 - "description": "Sort direction by createdAt." 31 - } 32 - } 33 - }, 34 - "output": { 35 - "encoding": "application/json", 36 - "schema": { 37 - "type": "object", 38 - "required": ["items"], 39 - "properties": { 40 - "items": { 41 - "type": "array", 42 - "items": { "type": "ref", "ref": "#listItem" } 43 - }, 44 - "cursor": { "type": "string" } 45 - } 46 - } 47 - } 48 - }, 49 - "listItem": { 50 - "type": "object", 51 - "required": ["uri", "value"], 52 - "properties": { 53 - "uri": { "type": "string", "format": "at-uri" }, 54 - "cid": { "type": "string", "format": "cid" }, 55 - "value": { 56 - "type": "unknown", 57 - "description": "Embedded sh.tangled.repo.issue.comment record" 58 - } 59 - } 60 - } 61 - } 62 - }
-50
bobbin/crates/types/lexicons/repo/issue/listCommentsBy.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.repo.issue.listCommentsBy", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "parameters": { 8 - "type": "params", 9 - "required": ["subject"], 10 - "properties": { 11 - "subject": { 12 - "type": "string", 13 - "format": "did", 14 - "description": "Actor DID whose issue-comment authorings to list." 15 - }, 16 - "cursor": { 17 - "type": "string", 18 - "description": "Pagination cursor" 19 - }, 20 - "limit": { 21 - "type": "integer", 22 - "minimum": 1, 23 - "maximum": 1000, 24 - "default": 50 25 - }, 26 - "order": { 27 - "type": "string", 28 - "knownValues": ["asc", "desc"], 29 - "default": "desc", 30 - "description": "Sort direction by createdAt." 31 - } 32 - } 33 - }, 34 - "output": { 35 - "encoding": "application/json", 36 - "schema": { 37 - "type": "object", 38 - "required": ["items"], 39 - "properties": { 40 - "items": { 41 - "type": "array", 42 - "items": { "type": "ref", "ref": "sh.tangled.repo.issue.listComments#listItem" } 43 - }, 44 - "cursor": { "type": "string" } 45 - } 46 - } 47 - } 48 - } 49 - } 50 - }
+3 -3
bobbin/crates/types/lexicons/repo/pull/listComments.json bobbin/crates/types/lexicons/feed/listComments.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "sh.tangled.repo.pull.listComments", 3 + "id": "sh.tangled.feed.listComments", 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", ··· 11 11 "subject": { 12 12 "type": "string", 13 13 "format": "at-uri", 14 - "description": "Pull AT-URI whose comments to list." 14 + "description": "Record AT-URI the comments are attached to." 15 15 }, 16 16 "cursor": { 17 17 "type": "string", ··· 54 54 "cid": { "type": "string", "format": "cid" }, 55 55 "value": { 56 56 "type": "unknown", 57 - "description": "Embedded sh.tangled.repo.pull.comment record" 57 + "description": "Embedded sh.tangled.feed.comment record" 58 58 } 59 59 } 60 60 }
+3 -3
bobbin/crates/types/lexicons/repo/pull/listCommentsBy.json bobbin/crates/types/lexicons/feed/listCommentsBy.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "sh.tangled.repo.pull.listCommentsBy", 3 + "id": "sh.tangled.feed.listCommentsBy", 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", ··· 11 11 "subject": { 12 12 "type": "string", 13 13 "format": "did", 14 - "description": "Actor DID whose pull-comment authorings to list." 14 + "description": "Actor DID whose comment authorings to list." 15 15 }, 16 16 "cursor": { 17 17 "type": "string", ··· 39 39 "properties": { 40 40 "items": { 41 41 "type": "array", 42 - "items": { "type": "ref", "ref": "sh.tangled.repo.pull.listComments#listItem" } 42 + "items": { "type": "ref", "ref": "sh.tangled.feed.listComments#listItem" } 43 43 }, 44 44 "cursor": { "type": "string" } 45 45 }
+43 -40
bobbin/crates/types/src/edges.rs
··· 9 9 10 10 use crate::ids::{SubjectRef, nsid_static}; 11 11 use crate::sh_tangled::actor::profile::Profile; 12 + use crate::sh_tangled::feed::comment::Comment as FeedCommentRecord; 12 13 use crate::sh_tangled::feed::reaction::Reaction; 13 14 use crate::sh_tangled::feed::star::Star; 14 15 use crate::sh_tangled::git::ref_update::RefUpdate; ··· 25 26 use crate::sh_tangled::repo::artifact::Artifact; 26 27 use crate::sh_tangled::repo::collaborator::Collaborator; 27 28 use crate::sh_tangled::repo::issue::Issue; 28 - use crate::sh_tangled::repo::issue::comment::Comment as IssueCommentRecord; 29 29 use crate::sh_tangled::repo::issue::state::State as IssueStateRecord; 30 30 use crate::sh_tangled::repo::pull::Pull; 31 - use crate::sh_tangled::repo::pull::comment::Comment as PullCommentRecord; 32 31 use crate::sh_tangled::repo::pull::status::Status as PullStatusRecord; 33 32 use crate::sh_tangled::spindle::Spindle; 34 33 use crate::sh_tangled::spindle::member::Member as SpindleMemberRecord; ··· 55 54 #[derive(Debug)] 56 55 pub enum Record { 57 56 Profile(Profile<DefaultStr>), 57 + FeedComment(FeedCommentRecord<DefaultStr>), 58 58 Reaction(Reaction<DefaultStr>), 59 59 Star(Star<DefaultStr>), 60 60 RefUpdate(RefUpdate<DefaultStr>), ··· 71 71 Artifact(Artifact<DefaultStr>), 72 72 Collaborator(Collaborator<DefaultStr>), 73 73 Issue(Issue<DefaultStr>), 74 - IssueComment(IssueCommentRecord<DefaultStr>), 75 74 IssueState(IssueStateRecord<DefaultStr>), 76 75 Pull(Pull<DefaultStr>), 77 - PullComment(PullCommentRecord<DefaultStr>), 78 76 PullStatus(PullStatusRecord<DefaultStr>), 79 77 Spindle(Spindle<DefaultStr>), 80 78 SpindleMember(SpindleMemberRecord<DefaultStr>), ··· 101 99 } 102 100 match nsid.as_ref() { 103 101 "sh.tangled.actor.profile" => parse!(Profile), 102 + "sh.tangled.feed.comment" => parse!(FeedComment), 104 103 "sh.tangled.feed.reaction" => parse!(Reaction), 105 104 "sh.tangled.feed.star" => parse!(Star), 106 105 "sh.tangled.git.refUpdate" => parse!(RefUpdate), ··· 117 116 "sh.tangled.repo.artifact" => parse!(Artifact), 118 117 "sh.tangled.repo.collaborator" => parse!(Collaborator), 119 118 "sh.tangled.repo.issue" => parse!(Issue), 120 - "sh.tangled.repo.issue.comment" => parse!(IssueComment), 121 119 "sh.tangled.repo.issue.state" => parse!(IssueState), 122 120 "sh.tangled.repo.pull" => parse!(Pull), 123 - "sh.tangled.repo.pull.comment" => parse!(PullComment), 124 121 "sh.tangled.repo.pull.status" => parse!(PullStatus), 125 122 "sh.tangled.spindle" => parse!(Spindle), 126 123 "sh.tangled.spindle.member" => parse!(SpindleMember), ··· 132 129 pub fn collection(&self) -> Nsid<DefaultStr> { 133 130 let s: &'static str = match self { 134 131 Self::Profile(_) => "sh.tangled.actor.profile", 132 + Self::FeedComment(_) => "sh.tangled.feed.comment", 135 133 Self::Reaction(_) => "sh.tangled.feed.reaction", 136 134 Self::Star(_) => "sh.tangled.feed.star", 137 135 Self::RefUpdate(_) => "sh.tangled.git.refUpdate", ··· 148 146 Self::Artifact(_) => "sh.tangled.repo.artifact", 149 147 Self::Collaborator(_) => "sh.tangled.repo.collaborator", 150 148 Self::Issue(_) => "sh.tangled.repo.issue", 151 - Self::IssueComment(_) => "sh.tangled.repo.issue.comment", 152 149 Self::IssueState(_) => "sh.tangled.repo.issue.state", 153 150 Self::Pull(_) => "sh.tangled.repo.pull", 154 - Self::PullComment(_) => "sh.tangled.repo.pull.comment", 155 151 Self::PullStatus(_) => "sh.tangled.repo.pull.status", 156 152 Self::Spindle(_) => "sh.tangled.spindle", 157 153 Self::SpindleMember(_) => "sh.tangled.spindle.member", ··· 185 181 fn created_at(&self) -> Option<&Datetime> { 186 182 match self { 187 183 Self::Profile(_) => None, 184 + Self::FeedComment(r) => Some(&r.created_at), 188 185 Self::Reaction(r) => Some(&r.created_at), 189 186 Self::Star(r) => Some(&r.created_at), 190 187 Self::RefUpdate(_) => None, ··· 201 198 Self::Artifact(r) => Some(&r.created_at), 202 199 Self::Collaborator(r) => Some(&r.created_at), 203 200 Self::Issue(r) => Some(&r.created_at), 204 - Self::IssueComment(r) => Some(&r.created_at), 205 201 Self::IssueState(_) => None, 206 202 Self::Pull(r) => Some(&r.created_at), 207 - Self::PullComment(r) => Some(&r.created_at), 208 203 Self::PullStatus(_) => None, 209 204 Self::Spindle(r) => Some(&r.created_at), 210 205 Self::SpindleMember(r) => Some(&r.created_at), ··· 215 210 fn primary_edges(&self, source: &AtUri<DefaultStr>) -> Result<Vec<Edge>, ExtractError> { 216 211 match self { 217 212 Self::Star(r) => star_edges(source, r), 213 + Self::FeedComment(r) => feed_comment_edges(source, r), 218 214 Self::Reaction(r) => reaction_edges(source, r), 219 215 Self::Follow(r) => follow_edges(source, r), 220 216 Self::RefUpdate(r) => ref_update_edges(source, r), ··· 224 220 Self::Artifact(r) => artifact_edges(source, r), 225 221 Self::Collaborator(r) => collaborator_edges(source, r), 226 222 Self::Issue(r) => issue_edges(source, r), 227 - Self::IssueComment(r) => issue_comment_edges(source, r), 228 223 Self::IssueState(r) => issue_state_edges(source, r), 229 224 Self::Pull(r) => pull_edges(source, r), 230 - Self::PullComment(r) => pull_comment_edges(source, r), 231 225 Self::PullStatus(r) => pull_status_edges(source, r), 232 226 Self::SpindleMember(r) => spindle_member_edges(source, r), 233 227 Self::Pipeline(r) => pipeline_edges(source, r), ··· 268 262 } 269 263 270 264 const MIRROR_KINDS: &[(&str, &str)] = &[ 265 + ("sh.tangled.feed.comment", "sh.tangled.feed.comment.by"), 271 266 ("sh.tangled.feed.star", "sh.tangled.feed.star.by"), 272 267 ("sh.tangled.feed.reaction", "sh.tangled.feed.reaction.by"), 273 268 ("sh.tangled.graph.follow", "sh.tangled.graph.follow.by"), ··· 287 282 ), 288 283 ("sh.tangled.repo.issue", "sh.tangled.repo.issue.by"), 289 284 ( 290 - "sh.tangled.repo.issue.comment", 291 - "sh.tangled.repo.issue.comment.by", 292 - ), 293 - ( 294 285 "sh.tangled.repo.issue.state", 295 286 "sh.tangled.repo.issue.state.by", 296 287 ), 297 288 ("sh.tangled.repo.pull", "sh.tangled.repo.pull.by"), 298 - ( 299 - "sh.tangled.repo.pull.comment", 300 - "sh.tangled.repo.pull.comment.by", 301 - ), 302 289 ( 303 290 "sh.tangled.repo.pull.status", 304 291 "sh.tangled.repo.pull.status.by", ··· 445 432 )) 446 433 } 447 434 448 - fn issue_comment_edges( 435 + fn feed_comment_edges( 449 436 source: &AtUri<DefaultStr>, 450 - record: &IssueCommentRecord<DefaultStr>, 437 + record: &FeedCommentRecord<DefaultStr>, 451 438 ) -> Result<Vec<Edge>, ExtractError> { 452 - let Some(subject) = uri_subject_for_record(&record.issue) else { 439 + let Some(subject) = uri_subject_for_record(&record.subject.uri) else { 453 440 return Ok(Vec::new()); 454 441 }; 455 - Ok(one_edge("sh.tangled.repo.issue.comment", subject, source)) 442 + Ok(one_edge("sh.tangled.feed.comment", subject, source)) 456 443 } 457 444 458 445 fn issue_state_edges( ··· 476 463 )) 477 464 } 478 465 479 - fn pull_comment_edges( 480 - source: &AtUri<DefaultStr>, 481 - record: &PullCommentRecord<DefaultStr>, 482 - ) -> Result<Vec<Edge>, ExtractError> { 483 - let Some(subject) = uri_subject_for_record(&record.pull) else { 484 - return Ok(Vec::new()); 485 - }; 486 - Ok(one_edge("sh.tangled.repo.pull.comment", subject, source)) 487 - } 488 - 489 466 fn pull_status_edges( 490 467 source: &AtUri<DefaultStr>, 491 468 record: &PullStatusRecord<DefaultStr>, ··· 674 651 } 675 652 676 653 #[test] 677 - fn issue_comment_uses_issue_uri() { 654 + fn feed_comment_keys_on_subject_uri() { 678 655 let edges = extract( 679 - "sh.tangled.repo.issue.comment", 680 - "at://did:plc:nel/sh.tangled.repo.issue.comment/abcabcabcabcz", 656 + "sh.tangled.feed.comment", 657 + "at://did:plc:nel/sh.tangled.feed.comment/abcabcabcabcz", 681 658 json!({ 682 - "$type": "sh.tangled.repo.issue.comment", 683 - "issue": "at://did:plc:nel/sh.tangled.repo.issue/3lk1", 684 - "body": "thoughts", 659 + "$type": "sh.tangled.feed.comment", 660 + "subject": { 661 + "uri": "at://did:plc:nel/sh.tangled.repo.issue/3lk1", 662 + "cid": "bafkqaaa" 663 + }, 664 + "body": { "$type": "sh.tangled.markup.markdown", "text": "thoughts" }, 685 665 "createdAt": "2026-05-01T00:00:00Z" 686 666 }), 687 667 ); ··· 689 669 assert_eq!( 690 670 edges[0].subject, 691 671 uri_subj("at://did:plc:nel/sh.tangled.repo.issue/3lk1") 672 + ); 673 + } 674 + 675 + #[test] 676 + fn feed_comment_on_pull_keys_on_pull_uri() { 677 + let edges = extract( 678 + "sh.tangled.feed.comment", 679 + "at://did:plc:nel/sh.tangled.feed.comment/abcabcabcabcz", 680 + json!({ 681 + "$type": "sh.tangled.feed.comment", 682 + "subject": { 683 + "uri": "at://did:plc:nel/sh.tangled.repo.pull/limpet", 684 + "cid": "bafkqaaa" 685 + }, 686 + "body": { "$type": "sh.tangled.markup.markdown", "text": "lgtm" }, 687 + "createdAt": "2026-05-01T00:00:00Z", 688 + "pullRoundIdx": 2 689 + }), 690 + ); 691 + assert_eq!(edges.len(), 1); 692 + assert_eq!( 693 + edges[0].subject, 694 + uri_subj("at://did:plc:nel/sh.tangled.repo.pull/limpet") 692 695 ); 693 696 } 694 697
+48
bobbin/crates/types/src/legacy.rs
··· 12 12 use crate::edges::ExtractError; 13 13 use crate::sh_tangled::repo::pull::Round as CanonRound; 14 14 15 + pub const LEGACY_COMMENT_SENTINEL_CID: &str = "bafkqaaa"; 16 + 15 17 fn empty_string_as_none<'de, D, T>(d: D) -> Result<Option<T>, D::Error> 16 18 where 17 19 D: Deserializer<'de>, ··· 117 119 #[derive(Debug, Deserialize)] 118 120 #[serde( 119 121 rename_all = "camelCase", 122 + rename = "sh.tangled.repo.issue.comment", 123 + tag = "$type", 124 + bound(deserialize = "S: Deserialize<'de> + BosStr") 125 + )] 126 + pub struct LegacyIssueComment<S: BosStr = DefaultStr> { 127 + pub created_at: Datetime, 128 + pub body: S, 129 + pub issue: AtUri<S>, 130 + #[serde(default, skip_serializing_if = "Option::is_none")] 131 + pub reply_to: Option<AtUri<S>>, 132 + #[serde(default, skip_serializing_if = "Option::is_none")] 133 + pub mentions: Option<Vec<Did<S>>>, 134 + #[serde(default, skip_serializing_if = "Option::is_none")] 135 + pub references: Option<Vec<AtUri<S>>>, 136 + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] 137 + pub extra_data: Option<BTreeMap<SmolStr, Data<S>>>, 138 + } 139 + 140 + #[derive(Debug, Deserialize)] 141 + #[serde( 142 + rename_all = "camelCase", 143 + rename = "sh.tangled.repo.pull.comment", 144 + tag = "$type", 145 + bound(deserialize = "S: Deserialize<'de> + BosStr") 146 + )] 147 + pub struct LegacyPullComment<S: BosStr = DefaultStr> { 148 + pub created_at: Datetime, 149 + pub body: S, 150 + pub pull: AtUri<S>, 151 + #[serde(default, skip_serializing_if = "Option::is_none")] 152 + pub mentions: Option<Vec<Did<S>>>, 153 + #[serde(default, skip_serializing_if = "Option::is_none")] 154 + pub references: Option<Vec<AtUri<S>>>, 155 + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] 156 + pub extra_data: Option<BTreeMap<SmolStr, Data<S>>>, 157 + } 158 + 159 + #[derive(Debug, Deserialize)] 160 + #[serde( 161 + rename_all = "camelCase", 120 162 rename = "sh.tangled.repo.collaborator", 121 163 tag = "$type", 122 164 bound(deserialize = "S: Deserialize<'de> + BosStr") ··· 224 266 #[derive(Debug)] 225 267 pub enum LegacyRecord { 226 268 Issue(LegacyIssue<DefaultStr>), 269 + IssueComment(LegacyIssueComment<DefaultStr>), 227 270 Pull(LegacyPull<DefaultStr>), 271 + PullComment(LegacyPullComment<DefaultStr>), 228 272 Collaborator(LegacyCollaborator<DefaultStr>), 229 273 RefUpdate(LegacyRefUpdate<DefaultStr>), 230 274 Star(LegacyStar<DefaultStr>), ··· 240 284 ) -> Result<Self, ExtractError> { 241 285 match nsid.as_ref() { 242 286 "sh.tangled.repo.issue" => Ok(Self::Issue(serde_json::from_slice(bytes)?)), 287 + "sh.tangled.repo.issue.comment" => { 288 + Ok(Self::IssueComment(serde_json::from_slice(bytes)?)) 289 + } 243 290 "sh.tangled.repo.pull" => Ok(Self::Pull(serde_json::from_slice(bytes)?)), 291 + "sh.tangled.repo.pull.comment" => Ok(Self::PullComment(serde_json::from_slice(bytes)?)), 244 292 "sh.tangled.repo.collaborator" => { 245 293 Ok(Self::Collaborator(serde_json::from_slice(bytes)?)) 246 294 }
+8 -24
bobbin/crates/types/src/search.rs
··· 11 11 use crate::edges::{ExtractError, Record}; 12 12 use crate::ids::nsid_static; 13 13 use crate::sh_tangled::actor::profile::Profile; 14 + use crate::sh_tangled::feed::comment::Comment as FeedCommentRecord; 14 15 use crate::sh_tangled::label::definition::Definition as LabelDefinitionRecord; 15 16 use crate::sh_tangled::repo::Repo as RepoRecord; 16 17 use crate::sh_tangled::repo::issue::Issue; 17 - use crate::sh_tangled::repo::issue::comment::Comment as IssueCommentRecord; 18 18 use crate::sh_tangled::repo::pull::Pull; 19 - use crate::sh_tangled::repo::pull::comment::Comment as PullCommentRecord; 20 19 use crate::sh_tangled::string::TangledString; 21 20 22 21 #[derive(Clone, Debug, Eq, PartialEq)] ··· 48 47 Profile(Profile<DefaultStr>), 49 48 Repo(RepoRecord<DefaultStr>), 50 49 Issue(Issue<DefaultStr>), 51 - IssueComment(IssueCommentRecord<DefaultStr>), 52 50 Pull(Pull<DefaultStr>), 53 - PullComment(PullCommentRecord<DefaultStr>), 51 + FeedComment(FeedCommentRecord<DefaultStr>), 54 52 TangledString(TangledString<DefaultStr>), 55 53 LabelDefinition(LabelDefinitionRecord<DefaultStr>), 56 54 } ··· 61 59 Record::Profile(r) => Some(Self::Profile(r)), 62 60 Record::Repo(r) => Some(Self::Repo(r)), 63 61 Record::Issue(r) => Some(Self::Issue(r)), 64 - Record::IssueComment(r) => Some(Self::IssueComment(r)), 65 62 Record::Pull(r) => Some(Self::Pull(r)), 66 - Record::PullComment(r) => Some(Self::PullComment(r)), 63 + Record::FeedComment(r) => Some(Self::FeedComment(r)), 67 64 Record::TangledString(r) => Some(Self::TangledString(r)), 68 65 Record::LabelDefinition(r) => Some(Self::LabelDefinition(r)), 69 66 Record::Reaction(_) ··· 100 97 Self::Profile(_) => "sh.tangled.actor.profile", 101 98 Self::Repo(_) => "sh.tangled.repo", 102 99 Self::Issue(_) => "sh.tangled.repo.issue", 103 - Self::IssueComment(_) => "sh.tangled.repo.issue.comment", 104 100 Self::Pull(_) => "sh.tangled.repo.pull", 105 - Self::PullComment(_) => "sh.tangled.repo.pull.comment", 101 + Self::FeedComment(_) => "sh.tangled.feed.comment", 106 102 Self::TangledString(_) => "sh.tangled.string", 107 103 Self::LabelDefinition(_) => "sh.tangled.label.definition", 108 104 }; ··· 114 110 Self::Profile(r) => profile_doc(source, r), 115 111 Self::Repo(r) => repo_doc(source, r), 116 112 Self::Issue(r) => issue_doc(source, r), 117 - Self::IssueComment(r) => issue_comment_doc(source, r), 118 113 Self::Pull(r) => pull_doc(source, r), 119 - Self::PullComment(r) => pull_comment_doc(source, r), 114 + Self::FeedComment(r) => feed_comment_doc(source, r), 120 115 Self::TangledString(r) => string_doc(source, r), 121 116 Self::LabelDefinition(r) => label_definition_doc(source, r), 122 117 } ··· 215 210 ) 216 211 } 217 212 218 - fn issue_comment_doc(source: &AtUri<DefaultStr>, r: &IssueCommentRecord<DefaultStr>) -> SearchDoc { 213 + fn feed_comment_doc(source: &AtUri<DefaultStr>, r: &FeedCommentRecord<DefaultStr>) -> SearchDoc { 219 214 doc( 220 215 source, 221 - "sh.tangled.repo.issue.comment", 216 + "sh.tangled.feed.comment", 222 217 "", 223 - Vec::from([r.body.as_str().to_owned()]), 218 + Vec::from([r.body.text.as_str().to_owned()]), 224 219 Some(r.created_at.timestamp()), 225 220 None, 226 221 ) ··· 239 234 body, 240 235 Some(r.created_at.timestamp()), 241 236 Some(r.target.repo.clone()), 242 - ) 243 - } 244 - 245 - fn pull_comment_doc(source: &AtUri<DefaultStr>, r: &PullCommentRecord<DefaultStr>) -> SearchDoc { 246 - doc( 247 - source, 248 - "sh.tangled.repo.pull.comment", 249 - "", 250 - Vec::from([r.body.as_str().to_owned()]), 251 - Some(r.created_at.timestamp()), 252 - None, 253 237 ) 254 238 } 255 239
+69 -92
bobbin/crates/xrpc/src/lib.rs
··· 37 37 use bobbin_types::record::RecordBody; 38 38 use bobbin_types::search::SearchableRecord; 39 39 use bobbin_types::sh_tangled::actor::profile::{Profile, ProfileGetRecordOutput, ProfileRecord}; 40 + use bobbin_types::sh_tangled::feed::comment::{ 41 + Comment as FeedComment, CommentRecord as FeedCommentRecord, 42 + }; 40 43 use bobbin_types::sh_tangled::feed::reaction::{Reaction, ReactionRecord}; 41 44 use bobbin_types::sh_tangled::feed::star::{Star, StarRecord}; 42 45 use bobbin_types::sh_tangled::git::ref_update::{RefUpdate, RefUpdateRecord}; ··· 57 60 use bobbin_types::sh_tangled::public_key::{PublicKey, PublicKeyRecord}; 58 61 use bobbin_types::sh_tangled::repo::artifact::{Artifact, ArtifactRecord}; 59 62 use bobbin_types::sh_tangled::repo::collaborator::{Collaborator, CollaboratorRecord}; 60 - use bobbin_types::sh_tangled::repo::issue::comment::{ 61 - Comment as IssueComment, CommentRecord as IssueCommentRecord, 62 - }; 63 63 use bobbin_types::sh_tangled::repo::issue::state::{ 64 64 State as IssueState, StateRecord as IssueStateRecord, 65 65 }; 66 66 use bobbin_types::sh_tangled::repo::issue::{Issue, IssueGetRecordOutput, IssueRecord}; 67 - use bobbin_types::sh_tangled::repo::pull::comment::{ 68 - Comment as PullComment, CommentRecord as PullCommentRecord, 69 - }; 70 67 use bobbin_types::sh_tangled::repo::pull::status::{ 71 68 Status as PullStatus, StatusRecord as PullStatusRecord, 72 69 }; ··· 181 178 .route("/xrpc/sh.tangled.repo.listPulls", get(list_pulls)) 182 179 .route("/xrpc/sh.tangled.repo.countPulls", get(count_pulls)) 183 180 .route( 184 - "/xrpc/sh.tangled.repo.issue.listComments", 185 - get(list_issue_comments), 186 - ) 187 - .route( 188 - "/xrpc/sh.tangled.repo.issue.countComments", 189 - get(count_issue_comments), 181 + "/xrpc/sh.tangled.feed.listComments", 182 + get(list_feed_comments), 190 183 ) 191 184 .route( 192 - "/xrpc/sh.tangled.repo.pull.listComments", 193 - get(list_pull_comments), 194 - ) 195 - .route( 196 - "/xrpc/sh.tangled.repo.pull.countComments", 197 - get(count_pull_comments), 185 + "/xrpc/sh.tangled.feed.countComments", 186 + get(count_feed_comments), 198 187 ) 199 188 .route("/xrpc/sh.tangled.feed.listReactions", get(list_reactions)) 200 189 .route("/xrpc/sh.tangled.feed.countReactions", get(count_reactions)) ··· 316 305 .route("/xrpc/sh.tangled.repo.listIssuesBy", get(list_issues_by)) 317 306 .route("/xrpc/sh.tangled.repo.countIssuesBy", get(count_issues_by)) 318 307 .route( 319 - "/xrpc/sh.tangled.repo.issue.listCommentsBy", 320 - get(list_issue_comments_by), 308 + "/xrpc/sh.tangled.feed.listCommentsBy", 309 + get(list_feed_comments_by), 321 310 ) 322 311 .route( 323 - "/xrpc/sh.tangled.repo.issue.countCommentsBy", 324 - get(count_issue_comments_by), 312 + "/xrpc/sh.tangled.feed.countCommentsBy", 313 + get(count_feed_comments_by), 325 314 ) 326 315 .route( 327 316 "/xrpc/sh.tangled.repo.issue.listStatesBy", ··· 333 322 ) 334 323 .route("/xrpc/sh.tangled.repo.listPullsBy", get(list_pulls_by)) 335 324 .route("/xrpc/sh.tangled.repo.countPullsBy", get(count_pulls_by)) 336 - .route( 337 - "/xrpc/sh.tangled.repo.pull.listCommentsBy", 338 - get(list_pull_comments_by), 339 - ) 340 - .route( 341 - "/xrpc/sh.tangled.repo.pull.countCommentsBy", 342 - get(count_pull_comments_by), 343 - ) 344 325 .route( 345 326 "/xrpc/sh.tangled.repo.pull.listStatusesBy", 346 327 get(list_pull_statuses_by), ··· 541 522 } 542 523 543 524 #[derive(Clone, Debug, Eq, PartialEq)] 544 - pub struct ExpectedNsid(Nsid<DefaultStr>); 525 + pub struct ExpectedNsid { 526 + canon: Nsid<DefaultStr>, 527 + aliases: &'static [&'static str], 528 + } 529 + 530 + const FEED_COMMENT_LEGACY_ALIASES: &[&str] = &[ 531 + "sh.tangled.repo.issue.comment", 532 + "sh.tangled.repo.pull.comment", 533 + ]; 534 + 535 + fn aliases_for(nsid: &str) -> &'static [&'static str] { 536 + match nsid { 537 + "sh.tangled.feed.comment" => FEED_COMMENT_LEGACY_ALIASES, 538 + _ => &[], 539 + } 540 + } 545 541 546 542 impl ExpectedNsid { 547 543 pub fn new(nsid: Nsid<DefaultStr>) -> Self { 548 - Self(nsid) 544 + let aliases = aliases_for(nsid.as_ref()); 545 + Self { 546 + canon: nsid, 547 + aliases, 548 + } 549 549 } 550 550 551 551 pub fn from_static(s: &'static str) -> Self { 552 - Self(nsid_static(s)) 552 + let canon = nsid_static(s); 553 + let aliases = aliases_for(s); 554 + Self { canon, aliases } 553 555 } 554 556 555 557 pub fn as_nsid(&self) -> &Nsid<DefaultStr> { 556 - &self.0 558 + &self.canon 557 559 } 558 560 559 561 pub fn as_str(&self) -> &str { 560 - self.0.as_ref() 562 + self.canon.as_ref() 563 + } 564 + 565 + fn accepts(&self, other: &str) -> bool { 566 + other == self.canon.as_ref() || self.aliases.contains(&other) 561 567 } 562 568 } 563 569 ··· 764 770 let repo_did = view.value.repo.clone(); 765 771 enrich_view( 766 772 &state.edges, 767 - nsid_static("sh.tangled.repo.issue.comment"), 773 + nsid_static("sh.tangled.feed.comment"), 768 774 &state.issue_states, 769 775 view, 770 776 move |src| accept_state_source(src, issue_author.as_ref(), &repo_did), ··· 779 785 let target_repo = view.value.target.repo.clone(); 780 786 enrich_view( 781 787 &state.edges, 782 - nsid_static("sh.tangled.repo.pull.comment"), 788 + nsid_static("sh.tangled.feed.comment"), 783 789 &state.pull_statuses, 784 790 view, 785 791 move |src| accept_state_source(src, pull_author.as_ref(), &target_repo), ··· 893 899 pub struct PipelineStatusBy; 894 900 pub struct ArtifactBy; 895 901 pub struct CollaboratorBy; 902 + pub struct FeedCommentBy; 896 903 pub struct IssueBy; 897 - pub struct IssueCommentBy; 898 904 pub struct IssueStateBy; 899 905 pub struct PullBy; 900 - pub struct PullCommentBy; 901 906 pub struct PullStatusBy; 902 907 pub struct SpindleMemberBy; 903 908 ··· 956 961 const EDGE_KIND: &'static str = "sh.tangled.repo.collaborator.by"; 957 962 const SHAPE: SubjectShape = SubjectShape::BareDid; 958 963 } 964 + impl MirrorOf for FeedCommentBy { 965 + type Record = FeedCommentRecord; 966 + const EDGE_KIND: &'static str = "sh.tangled.feed.comment.by"; 967 + const SHAPE: SubjectShape = SubjectShape::BareDid; 968 + } 959 969 impl MirrorOf for IssueBy { 960 970 type Record = IssueRecord; 961 971 const EDGE_KIND: &'static str = "sh.tangled.repo.issue.by"; 962 972 const SHAPE: SubjectShape = SubjectShape::BareDid; 963 973 } 964 - impl MirrorOf for IssueCommentBy { 965 - type Record = IssueCommentRecord; 966 - const EDGE_KIND: &'static str = "sh.tangled.repo.issue.comment.by"; 967 - const SHAPE: SubjectShape = SubjectShape::BareDid; 968 - } 969 974 impl MirrorOf for IssueStateBy { 970 975 type Record = IssueStateRecord; 971 976 const EDGE_KIND: &'static str = "sh.tangled.repo.issue.state.by"; ··· 974 979 impl MirrorOf for PullBy { 975 980 type Record = PullRecord; 976 981 const EDGE_KIND: &'static str = "sh.tangled.repo.pull.by"; 977 - const SHAPE: SubjectShape = SubjectShape::BareDid; 978 - } 979 - impl MirrorOf for PullCommentBy { 980 - type Record = PullCommentRecord; 981 - const EDGE_KIND: &'static str = "sh.tangled.repo.pull.comment.by"; 982 982 const SHAPE: SubjectShape = SubjectShape::BareDid; 983 983 } 984 984 impl MirrorOf for PullStatusBy { ··· 1004 1004 impl HasSubject for PullRecord { 1005 1005 const SHAPE: SubjectShape = SubjectShape::BareDid; 1006 1006 } 1007 - impl HasSubject for IssueCommentRecord { 1008 - const SHAPE: SubjectShape = SubjectShape::Collection("sh.tangled.repo.issue"); 1009 - } 1010 - impl HasSubject for PullCommentRecord { 1011 - const SHAPE: SubjectShape = SubjectShape::Collection("sh.tangled.repo.pull"); 1007 + impl HasSubject for FeedCommentRecord { 1008 + const SHAPE: SubjectShape = 1009 + SubjectShape::OneOfCollections(&["sh.tangled.repo.issue", "sh.tangled.repo.pull"]); 1012 1010 } 1013 1011 impl HasSubject for LabelDefinitionRecord { 1014 1012 const SHAPE: SubjectShape = SubjectShape::BareDid; ··· 1177 1175 let collection = uri 1178 1176 .collection() 1179 1177 .ok_or_else(|| XrpcError::InvalidParams("uri missing collection".into()))?; 1180 - if collection.as_ref() != expected.as_str() { 1178 + if !expected.accepts(collection.as_ref()) { 1181 1179 return Err(XrpcError::InvalidParams(format!( 1182 1180 "collection mismatch: expected {}, got {}", 1183 1181 expected.as_str(), ··· 1231 1229 .ok_or_else(|| XrpcError::InvalidRecord("$type peek: missing $type field".into()))? 1232 1230 } 1233 1231 }; 1234 - if ty.as_ref() != expected.as_str() { 1232 + if !expected.accepts(ty.as_ref()) { 1235 1233 return Err(XrpcError::InvalidRecord(format!( 1236 1234 "$type mismatch: expected {}, got {}", 1237 1235 expected.as_str(), ··· 1239 1237 ))); 1240 1238 } 1241 1239 Ok(()) 1240 + } 1241 + 1242 + fn wire_type_nsid(bytes: &[u8]) -> Option<Nsid<DefaultStr>> { 1243 + let ty = serde_json::from_slice::<TypeTag>(bytes).ok()?.ty; 1244 + Nsid::<DefaultStr>::new_owned(ty).ok() 1242 1245 } 1243 1246 1244 1247 async fn deserialize_or_upgrade<V>( ··· 1266 1269 { 1267 1270 return Ok(v); 1268 1271 } 1269 - match upgrade_wire_bytes(nsid, retry_bytes, &state.resolver).await { 1272 + let wire_nsid = wire_type_nsid(retry_bytes).unwrap_or_else(|| nsid.clone()); 1273 + match upgrade_wire_bytes(&wire_nsid, retry_bytes, &state.resolver).await { 1270 1274 Ok(canon_bytes) => serde_json::from_slice(&canon_bytes) 1271 1275 .map_err(|e| XrpcError::InvalidRecord(e.to_string())), 1272 1276 Err(_) => Err(XrpcError::InvalidRecord(canon_err.to_string())), ··· 1864 1868 count_for::<PullRecord>(&state, q).map(Json) 1865 1869 } 1866 1870 1867 - async fn list_issue_comments( 1868 - State(state): State<AppState>, 1869 - XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1870 - ) -> Result<Response, XrpcError> { 1871 - list_records::<IssueCommentRecord, IssueComment<DefaultStr>, _>(&state, q).await 1872 - } 1873 - 1874 - async fn count_issue_comments( 1875 - State(state): State<AppState>, 1876 - XrpcQuery(q): XrpcQuery<CountQuery>, 1877 - ) -> Result<Json<CountResponse>, XrpcError> { 1878 - count_for::<IssueCommentRecord>(&state, q).map(Json) 1879 - } 1880 - 1881 - async fn list_pull_comments( 1871 + async fn list_feed_comments( 1882 1872 State(state): State<AppState>, 1883 1873 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 1884 1874 ) -> Result<Response, XrpcError> { 1885 - list_records::<PullCommentRecord, PullComment<DefaultStr>, _>(&state, q).await 1875 + list_records::<FeedCommentRecord, FeedComment<DefaultStr>, _>(&state, q).await 1886 1876 } 1887 1877 1888 - async fn count_pull_comments( 1878 + async fn count_feed_comments( 1889 1879 State(state): State<AppState>, 1890 1880 XrpcQuery(q): XrpcQuery<CountQuery>, 1891 1881 ) -> Result<Json<CountResponse>, XrpcError> { 1892 - count_for::<PullCommentRecord>(&state, q).map(Json) 1882 + count_for::<FeedCommentRecord>(&state, q).map(Json) 1893 1883 } 1894 1884 1895 1885 async fn list_reactions( ··· 2203 2193 count_mirror::<IssueBy>(&state, q).map(Json) 2204 2194 } 2205 2195 2206 - async fn list_issue_comments_by( 2196 + async fn list_feed_comments_by( 2207 2197 State(state): State<AppState>, 2208 2198 XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2209 2199 ) -> Result<Response, XrpcError> { 2210 - list_mirror::<IssueCommentBy, IssueComment<DefaultStr>, _>(&state, q).await 2200 + list_mirror::<FeedCommentBy, FeedComment<DefaultStr>, _>(&state, q).await 2211 2201 } 2212 - async fn count_issue_comments_by( 2202 + async fn count_feed_comments_by( 2213 2203 State(state): State<AppState>, 2214 2204 XrpcQuery(q): XrpcQuery<CountQuery>, 2215 2205 ) -> Result<Json<CountResponse>, XrpcError> { 2216 - count_mirror::<IssueCommentBy>(&state, q).map(Json) 2206 + count_mirror::<FeedCommentBy>(&state, q).map(Json) 2217 2207 } 2218 2208 2219 2209 async fn list_issue_states_by( ··· 2251 2241 XrpcQuery(q): XrpcQuery<CountQuery>, 2252 2242 ) -> Result<Json<CountResponse>, XrpcError> { 2253 2243 count_mirror::<PullBy>(&state, q).map(Json) 2254 - } 2255 - 2256 - async fn list_pull_comments_by( 2257 - State(state): State<AppState>, 2258 - XrpcQuery(q): XrpcQuery<TypedListQuery<NoFilter>>, 2259 - ) -> Result<Response, XrpcError> { 2260 - list_mirror::<PullCommentBy, PullComment<DefaultStr>, _>(&state, q).await 2261 - } 2262 - async fn count_pull_comments_by( 2263 - State(state): State<AppState>, 2264 - XrpcQuery(q): XrpcQuery<CountQuery>, 2265 - ) -> Result<Json<CountResponse>, XrpcError> { 2266 - count_mirror::<PullCommentBy>(&state, q).map(Json) 2267 2244 } 2268 2245 2269 2246 async fn list_pull_statuses_by(
+33 -33
bobbin/crates/xrpc/tests/aggregation.rs
··· 804 804 "sh.tangled.repo.countIssues", 805 805 "sh.tangled.repo.listPulls", 806 806 "sh.tangled.repo.countPulls", 807 - "sh.tangled.repo.issue.listComments", 808 - "sh.tangled.repo.issue.countComments", 807 + "sh.tangled.feed.listComments", 808 + "sh.tangled.feed.countComments", 809 809 ]; 810 810 stream::iter(cases) 811 811 .for_each(|endpoint| { ··· 893 893 } 894 894 895 895 #[tokio::test] 896 - async fn list_issue_comments_hydrates_end_to_end() { 896 + async fn list_feed_comments_hydrates_end_to_end() { 897 897 let h = Harness::new().await; 898 898 let issue_uri = at("at://did:plc:abalone/sh.tangled.repo.issue/i1"); 899 899 let nel = did("did:plc:nel"); 900 900 let rk = rkey("c1"); 901 901 h.add_edge( 902 - &nsid("sh.tangled.repo.issue.comment"), 902 + &nsid("sh.tangled.feed.comment"), 903 903 &issue_uri, 904 904 &at(&format!( 905 - "at://{}/sh.tangled.repo.issue.comment/{}", 905 + "at://{}/sh.tangled.feed.comment/{}", 906 906 nel.as_ref(), 907 907 rk.as_ref() 908 908 )), 909 909 ); 910 910 h.mount( 911 911 &nel, 912 - &nsid("sh.tangled.repo.issue.comment"), 912 + &nsid("sh.tangled.feed.comment"), 913 913 &rk, 914 914 json!({ 915 - "$type": "sh.tangled.repo.issue.comment", 916 - "issue": issue_uri.as_ref(), 917 - "body": "thoughts", 915 + "$type": "sh.tangled.feed.comment", 916 + "subject": { "uri": issue_uri.as_ref(), "cid": "bafkqaaa" }, 917 + "body": { "$type": "sh.tangled.markup.markdown", "text": "thoughts" }, 918 918 "createdAt": "2026-05-01T00:00:00Z" 919 919 }), 920 920 ) ··· 923 923 let app = router(h.state.clone()); 924 924 let (status, body) = json_response( 925 925 app.oneshot(list_request( 926 - "sh.tangled.repo.issue.listComments", 926 + "sh.tangled.feed.listComments", 927 927 issue_uri.as_ref(), 928 928 &[], 929 929 )) ··· 934 934 assert_eq!(status, StatusCode::OK); 935 935 let items = body["items"].as_array().unwrap(); 936 936 assert_eq!(items.len(), 1); 937 - assert_eq!(items[0]["value"]["body"], json!("thoughts")); 938 - assert_eq!(items[0]["value"]["issue"], json!(issue_uri.as_ref())); 937 + assert_eq!(items[0]["value"]["body"]["text"], json!("thoughts")); 938 + assert_eq!( 939 + items[0]["value"]["subject"]["uri"], 940 + json!(issue_uri.as_ref()) 941 + ); 939 942 } 940 943 941 944 #[tokio::test] ··· 976 979 } 977 980 978 981 #[tokio::test] 979 - async fn count_issue_comments_subjects_on_issue_uri() { 982 + async fn count_feed_comments_subjects_on_issue_uri() { 980 983 let h = Harness::new().await; 981 984 let issue_uri = at("at://did:plc:abalone/sh.tangled.repo.issue/i1"); 982 985 h.add_edge( 983 - &nsid("sh.tangled.repo.issue.comment"), 986 + &nsid("sh.tangled.feed.comment"), 984 987 &issue_uri, 985 - &at("at://did:plc:nel/sh.tangled.repo.issue.comment/c1"), 988 + &at("at://did:plc:nel/sh.tangled.feed.comment/c1"), 986 989 ); 987 990 h.add_edge( 988 - &nsid("sh.tangled.repo.issue.comment"), 991 + &nsid("sh.tangled.feed.comment"), 989 992 &issue_uri, 990 - &at("at://did:plc:olaren/sh.tangled.repo.issue.comment/c2"), 993 + &at("at://did:plc:olaren/sh.tangled.feed.comment/c2"), 991 994 ); 992 995 993 996 let app = router(h.state.clone()); 994 997 let (status, body) = json_response( 995 998 app.oneshot(list_request( 996 - "sh.tangled.repo.issue.countComments", 999 + "sh.tangled.feed.countComments", 997 1000 issue_uri.as_ref(), 998 1001 &[], 999 1002 )) ··· 1283 1286 } 1284 1287 1285 1288 #[tokio::test] 1286 - async fn issue_collection_endpoints_reject_bare_did_or_wrong_collection() { 1289 + async fn feed_comment_endpoints_reject_bare_did_or_wrong_collection() { 1287 1290 let h = Harness::new().await; 1288 1291 let app = router(h.state.clone()); 1289 1292 let endpoints = [ 1290 - "sh.tangled.repo.issue.listComments", 1291 - "sh.tangled.repo.issue.countComments", 1293 + "sh.tangled.feed.listComments", 1294 + "sh.tangled.feed.countComments", 1292 1295 ]; 1293 1296 let inputs = [ 1294 1297 "at://did:plc:abalone", ··· 1308 1311 .unwrap(); 1309 1312 let (status, body) = json_response(resp).await; 1310 1313 assert_eq!(status, StatusCode::BAD_REQUEST, "{endpoint} input={input}"); 1314 + let msg = body["message"].as_str().unwrap_or_default(); 1311 1315 assert!( 1312 - body["message"] 1313 - .as_str() 1314 - .unwrap_or_default() 1315 - .contains("sh.tangled.repo.issue/<rkey>"), 1316 - "{endpoint} input={input}: {}", 1317 - body["message"], 1316 + msg.contains("sh.tangled.repo.issue") && msg.contains("sh.tangled.repo.pull"), 1317 + "{endpoint} input={input}: {msg}", 1318 1318 ); 1319 1319 } 1320 1320 }) ··· 1556 1556 ) 1557 1557 .await; 1558 1558 h.add_edge( 1559 - &nsid("sh.tangled.repo.issue.comment"), 1559 + &nsid("sh.tangled.feed.comment"), 1560 1560 &issue_uri, 1561 - &at("at://did:plc:olaren/sh.tangled.repo.issue.comment/c1"), 1561 + &at("at://did:plc:olaren/sh.tangled.feed.comment/c1"), 1562 1562 ); 1563 1563 h.add_edge( 1564 - &nsid("sh.tangled.repo.issue.comment"), 1564 + &nsid("sh.tangled.feed.comment"), 1565 1565 &issue_uri, 1566 - &at("at://did:plc:teq/sh.tangled.repo.issue.comment/c2"), 1566 + &at("at://did:plc:teq/sh.tangled.feed.comment/c2"), 1567 1567 ); 1568 1568 1569 1569 h.state.issue_states.upsert( ··· 1731 1731 ) 1732 1732 .await; 1733 1733 h.add_edge( 1734 - &nsid("sh.tangled.repo.pull.comment"), 1734 + &nsid("sh.tangled.feed.comment"), 1735 1735 &pull_uri, 1736 - &at("at://did:plc:teq/sh.tangled.repo.pull.comment/c1"), 1736 + &at("at://did:plc:teq/sh.tangled.feed.comment/c1"), 1737 1737 ); 1738 1738 h.state.pull_statuses.upsert( 1739 1739 at("at://did:plc:nel/sh.tangled.repo.pull.status/s1"),