1mod references;
4mod tags;
5use std::collections::BTreeSet;
6
7use crypto::{Signer, thread_operation::SignedOperation};
8use heddle_object_model::object::{
9 Attribution, CollabOpId, CollaborationIdempotencyKey, CollaborationMetadata,
10 CollaborationOperationBodyV1, CollaborationOperationEnvelope, ContentHash, ContextRevision,
11 DiscussionRecordId,
12 thread_replication::{OPERATION_FORMAT, ThreadOperation, ThreadOperationBody},
13};
14pub use references::{
15 anchor, anchor_ref, audience, mention, mention_ref, source_target_ref, visibility,
16};
17pub use tags::{
18 annotation_query, annotation_source, annotation_source_ref, annotation_tag, annotation_tag_ref,
19 annotation_tags, annotation_value, annotation_value_ref,
20};
21
22use crate::{
23 contract::{RecordSignature, SignedRecord},
24 transport::Error,
25};
26
27pub fn verify(record: &SignedRecord) -> Result<ThreadOperation, Error> {
30 if record.format != OPERATION_FORMAT || record.signatures.len() != 1 {
31 return Err(Error::Protocol("unsupported collaboration signed record"));
32 }
33 let operation = SignedOperation {
34 canonical: record.canonical_record.clone(),
35 signature: record.signatures[0].signature.clone(),
36 }
37 .verify()
38 .map_err(|_| Error::Protocol("invalid collaboration author signature"))?;
39 if record.signatures[0].public_key != operation.publisher {
40 return Err(Error::Protocol(
41 "collaboration signature key differs from publisher",
42 ));
43 }
44 if !matches!(
45 operation.body,
46 ThreadOperationBody::Discussion(_) | ThreadOperationBody::Context(_)
47 ) {
48 return Err(Error::Protocol(
49 "collaboration requires a discussion or context operation",
50 ));
51 }
52 Ok(operation)
53}
54
55pub struct Command {
58 pub discussion: DiscussionRecordId,
59 pub operation_id: CollaborationIdempotencyKey,
60 pub metadata: CollaborationMetadata,
61 pub author: Attribution,
62 pub occurred_at_ms: i64,
63 pub body: CollaborationOperationBodyV1,
64}
65
66impl Command {
67 pub fn sign(
71 self,
72 parents: &[SignedRecord],
73 signer: &impl Signer,
74 ) -> Result<SignedRecord, Error> {
75 if parents.len() > 128 {
76 return Err(Error::Protocol(
77 "collaboration has more than 128 causal parents",
78 ));
79 }
80 let thread = self.metadata.scope.thread.ok_or(Error::Protocol(
81 "Thread command requires native Thread scope",
82 ))?;
83 let mut outer = BTreeSet::new();
84 let mut inner = BTreeSet::new();
85 for record in parents {
86 let operation = verify(record)?;
87 if operation.thread != thread {
88 return Err(Error::Protocol("parent belongs to another Thread"));
89 }
90 let ThreadOperationBody::Discussion(bytes) = &operation.body else {
91 return Err(Error::Protocol("parent is not a collaboration operation"));
92 };
93 let decoded = CollaborationOperationEnvelope::decode(bytes)
94 .map_err(|_| Error::Protocol("invalid collaboration parent"))?;
95 if decoded.operation.discussion_id != self.discussion
96 || decoded
97 .operation
98 .metadata
99 .as_ref()
100 .is_none_or(|m| m.scope != self.metadata.scope)
101 {
102 return Err(Error::Protocol(
103 "parent belongs to another discussion or spool",
104 ));
105 }
106 let id = operation
107 .id()
108 .map_err(|error| Error::Io(error.to_string()))?;
109 if !outer.insert(id) {
110 return Err(Error::Protocol("duplicate collaboration causal parent"));
111 }
112 inner.insert(decoded.operation_id);
113 }
114 self.sign_frontier(thread, outer, inner, signer)
115 }
116
117 pub fn sign_parent_ids(
122 self,
123 parent_ids: &[ContentHash],
124 signer: &impl Signer,
125 ) -> Result<SignedRecord, Error> {
126 if parent_ids.len() > 128 {
127 return Err(Error::Protocol(
128 "collaboration has more than 128 causal parents",
129 ));
130 }
131 let thread = self.metadata.scope.thread.ok_or(Error::Protocol(
132 "Thread command requires native Thread scope",
133 ))?;
134 let mut outer = BTreeSet::new();
135 let mut inner = BTreeSet::new();
136 for id in parent_ids {
137 if !outer.insert(*id) {
138 return Err(Error::Protocol("duplicate collaboration causal parent"));
139 }
140 inner.insert(CollabOpId::from_bytes(*id.as_bytes()));
141 }
142 self.sign_frontier(thread, outer, inner, signer)
143 }
144
145 fn sign_frontier(
146 self,
147 thread: ContentHash,
148 outer: BTreeSet<ContentHash>,
149 inner: BTreeSet<CollabOpId>,
150 signer: &impl Signer,
151 ) -> Result<SignedRecord, Error> {
152 let mut body = self.body;
153 if let CollaborationOperationBodyV1::Resolve {
154 resolution:
155 heddle_object_model::object::CollaborationResolution::IntoContext { context },
156 } = &mut body
157 {
158 context.parents = outer.iter().copied().collect();
159 }
160 let envelope = CollaborationOperationEnvelope::new(
161 self.discussion,
162 inner.into_iter().collect(),
163 self.operation_id,
164 self.author,
165 self.occurred_at_ms,
166 body,
167 )
168 .and_then(|envelope| envelope.with_metadata(self.metadata))
169 .map_err(|error| Error::Io(error.to_string()))?;
170 let operation = ThreadOperation {
171 version: 1,
172 thread,
173 parents: outer,
174 publisher: signer
175 .public_key()
176 .try_into()
177 .map_err(|_| Error::Protocol("collaboration requires an Ed25519 signer"))?,
178 body: ThreadOperationBody::Discussion(
179 envelope
180 .encode()
181 .map_err(|error| Error::Io(error.to_string()))?,
182 ),
183 };
184 let signed = SignedOperation::sign(&operation, signer)
185 .map_err(|error| Error::Io(error.to_string()))?;
186 Ok(SignedRecord {
187 format: OPERATION_FORMAT.into(),
188 canonical_record: signed.canonical,
189 signatures: vec![RecordSignature {
190 public_key: operation.publisher.to_vec(),
191 signature: signed.signature,
192 }],
193 })
194 }
195}
196
197pub fn sign_context(
200 context: ContextRevision,
201 parents: &[SignedRecord],
202 signer: &impl Signer,
203) -> Result<SignedRecord, Error> {
204 if parents.len() > 128 {
205 return Err(Error::Protocol("context has more than 128 causal parents"));
206 }
207 let thread = context.metadata.scope.thread.ok_or(Error::Protocol(
208 "Thread context requires native Thread scope",
209 ))?;
210 if parents.is_empty() && context.extracted_from.is_some() {
211 return Err(Error::Protocol(
212 "context extraction requires a signed discussion resolution",
213 ));
214 }
215 let mut ids = BTreeSet::new();
216 for record in parents {
217 let parent = verify(record)?;
218 let previous = parent
219 .context_revision()
220 .map_err(|error| Error::Io(error.to_string()))?
221 .ok_or(Error::Protocol(
222 "context parent is not a context revision or extraction",
223 ))?;
224 if parent.thread != thread
225 || previous.id != context.id
226 || previous.metadata.scope != context.metadata.scope
227 || previous.extracted_from != context.extracted_from
228 {
229 return Err(Error::Protocol(
230 "context parent belongs to another record or scope",
231 ));
232 }
233 if !ids.insert(parent.id().map_err(|error| Error::Io(error.to_string()))?) {
234 return Err(Error::Protocol("duplicate context causal parent"));
235 }
236 }
237 sign_context_ids(context, thread, ids, signer)
238}
239
240fn sign_context_ids(
241 mut context: ContextRevision,
242 thread: ContentHash,
243 ids: BTreeSet<ContentHash>,
244 signer: &impl Signer,
245) -> Result<SignedRecord, Error> {
246 context.parents = ids.iter().copied().collect();
247 let operation = ThreadOperation {
248 version: 1,
249 thread,
250 parents: ids,
251 publisher: signer
252 .public_key()
253 .try_into()
254 .map_err(|_| Error::Protocol("context requires an Ed25519 signer"))?,
255 body: ThreadOperationBody::Context(
256 context
257 .encode()
258 .map_err(|error| Error::Io(error.to_string()))?,
259 ),
260 };
261 let signed =
262 SignedOperation::sign(&operation, signer).map_err(|error| Error::Io(error.to_string()))?;
263 Ok(SignedRecord {
264 format: OPERATION_FORMAT.into(),
265 canonical_record: signed.canonical,
266 signatures: vec![RecordSignature {
267 public_key: operation.publisher.to_vec(),
268 signature: signed.signature,
269 }],
270 })
271}
272
273pub fn operation_id(record: &SignedRecord) -> Result<ContentHash, Error> {
275 verify(record)?
276 .id()
277 .map_err(|error| Error::Io(error.to_string()))
278}
279
280#[cfg(test)]
281mod tests {
282 use crypto::Ed25519Signer;
283 use heddle_object_model::object::{
284 CollaborationActor, CollaborationAnchor, CollaborationScope, DiscussionTurnV1, Principal,
285 VisibilityTier,
286 };
287 use uuid::Uuid;
288
289 use super::*;
290
291 fn command(discussion: DiscussionRecordId, body: CollaborationOperationBodyV1) -> Command {
292 Command {
293 discussion,
294 operation_id: CollaborationIdempotencyKey::new("operation-1").expect("operation ID"),
295 metadata: CollaborationMetadata {
296 scope: CollaborationScope {
297 spool: Uuid::from_u128(1),
298 thread: Some(ContentHash::from_bytes([2; 32])),
299 },
300 actor: CollaborationActor {
301 principal_id: Uuid::from_u128(3),
302 agent_id: Some("agent-4".into()),
303 },
304 mentions: vec![],
305 },
306 author: Attribution::human(Principal::new("Account", "")),
307 occurred_at_ms: 100,
308 body,
309 }
310 }
311 fn append(body: &str) -> CollaborationOperationBodyV1 {
312 CollaborationOperationBodyV1::AppendTurn {
313 turn: DiscussionTurnV1::new(body).expect("turn"),
314 }
315 }
316 #[test]
317 fn signed_parents_preserve_concurrent_heads_and_reject_changed_proofs() {
318 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
319 let discussion = DiscussionRecordId::generate();
320 let root = command(
321 discussion,
322 CollaborationOperationBodyV1::Open {
323 tags: Vec::new(),
324 blocking: false,
325 title: "Review".into(),
326 anchor: CollaborationAnchor::Repository,
327 visibility: VisibilityTier::Private {
328 scope_label: "owner".into(),
329 },
330 turn: DiscussionTurnV1::new("Review this Thread").expect("turn"),
331 thread_ref: None,
332 },
333 )
334 .sign(&[], &signer)
335 .expect("root");
336 let left = command(discussion, append("left"))
337 .sign(std::slice::from_ref(&root), &signer)
338 .expect("left");
339 let right = command(discussion, append("right"))
340 .sign(std::slice::from_ref(&root), &signer)
341 .expect("right");
342 assert_ne!(
343 operation_id(&left).expect("left ID"),
344 operation_id(&right).expect("right ID")
345 );
346 let joined = command(discussion, append("both observed"))
347 .sign(&[left.clone(), right.clone()], &signer)
348 .expect("join");
349 assert_eq!(
350 verify(&joined).expect("verified").parents,
351 BTreeSet::from([
352 operation_id(&left).expect("left ID"),
353 operation_id(&right).expect("right ID")
354 ])
355 );
356 let mut changed = root.clone();
357 changed.signatures[0].signature[0] ^= 1;
358 assert!(
359 command(discussion, append("bad proof"))
360 .sign(&[changed], &signer)
361 .is_err()
362 );
363 assert!(
364 command(DiscussionRecordId::generate(), append("wrong discussion"))
365 .sign(std::slice::from_ref(&root), &signer)
366 .is_err()
367 );
368 assert!(
369 command(discussion, append("duplicate parent"))
370 .sign(&[root.clone(), root], &signer)
371 .is_err()
372 );
373 }
374 #[test]
375 fn observed_parent_ids_are_sorted_unique_and_required_for_append() {
376 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
377 let discussion = DiscussionRecordId::generate();
378 let root = command(
379 discussion,
380 CollaborationOperationBodyV1::Open {
381 tags: Vec::new(),
382 blocking: false,
383 title: "Review".into(),
384 anchor: CollaborationAnchor::Repository,
385 visibility: VisibilityTier::Private {
386 scope_label: "owner".into(),
387 },
388 turn: DiscussionTurnV1::new("Review this Thread").expect("turn"),
389 thread_ref: None,
390 },
391 )
392 .sign(&[], &signer)
393 .expect("root");
394 let root_id = operation_id(&root).expect("root ID");
395 let appended = command(discussion, append("follow-up"))
396 .sign_parent_ids(&[root_id], &signer)
397 .expect("append");
398 assert_eq!(
399 verify(&appended).expect("verified").parents,
400 BTreeSet::from([root_id])
401 );
402 assert!(
403 command(discussion, append("no parent"))
404 .sign_parent_ids(&[], &signer)
405 .is_err()
406 );
407 }
408 #[test]
409 fn context_revisions_retain_history_and_bind_parent_record_identity() {
410 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
411 let context = ContextRevision {
412 version: 2,
413 id: Uuid::from_u128(9),
414 parents: vec![],
415 metadata: command(DiscussionRecordId::generate(), append("metadata")).metadata,
416 anchor: CollaborationAnchor::Repository,
417 content: "Original rationale".into(),
418 tags: vec!["decision".into()],
419 supersedes: None,
420 extracted_from: None,
421 occurred_at_ms: 100,
422 provenance: None,
423 canonical_body: Default::default(),
424 };
425 let first = sign_context(context.clone(), &[], &signer).expect("first context");
426 let mut revised = context.clone();
427 revised.content = "Revised rationale".into();
428 let second =
429 sign_context(revised.clone(), std::slice::from_ref(&first), &signer).expect("revision");
430 assert_eq!(
431 verify(&second).expect("second").parents,
432 BTreeSet::from([operation_id(&first).expect("first ID")])
433 );
434 let ThreadOperationBody::Context(bytes) = verify(&first).expect("first").body else {
435 panic!("context");
436 };
437 assert_eq!(
438 ContextRevision::decode(&bytes)
439 .expect("original context")
440 .content,
441 "Original rationale"
442 );
443 revised.id = Uuid::from_u128(10);
444 assert!(
445 sign_context(revised, std::slice::from_ref(&first), &signer).is_err(),
446 "parent cannot silently change record identity"
447 );
448 }
449 #[test]
450 fn extracted_context_keeps_original_resolution_proof_and_actor_binding() {
451 use heddle_object_model::object::{
452 CollaborationResolution, StateId, thread_replication::ThreadGenesis,
453 };
454 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
455 let genesis = ThreadGenesis {
456 version: 1,
457 spool: Uuid::from_u128(1).to_string(),
458 parent: None,
459 base: StateId::from_bytes([3; 32]),
460 name: "extraction".into(),
461 intent: "retain proof".into(),
462 creator: signer.public_key().try_into().expect("key"),
463 owner: heddle_object_model::object::thread_replication::GenesisOwner::LocalKey(
464 signer.public_key().try_into().expect("key"),
465 ),
466 nonce: vec![1; 16],
467 };
468 let discussion = DiscussionRecordId::generate();
469 let make = |body| {
470 let mut command = command(discussion, body);
471 command.metadata.scope.thread = Some(genesis.id().expect("Thread"));
472 command
473 };
474 let open = make(CollaborationOperationBodyV1::Open {
475 tags: Vec::new(),
476 blocking: true,
477 title: "Review".into(),
478 anchor: CollaborationAnchor::Repository,
479 visibility: VisibilityTier::Public,
480 turn: DiscussionTurnV1::new("Evidence").expect("turn"),
481 thread_ref: None,
482 })
483 .sign(&[], &signer)
484 .expect("root");
485 let mut context = ContextRevision {
486 version: 2,
487 id: Uuid::from_u128(9),
488 parents: vec![],
489 metadata: make(append("metadata")).metadata,
490 anchor: CollaborationAnchor::Repository,
491 content: "Durable rationale".into(),
492 tags: vec!["decision".into()],
493 supersedes: None,
494 extracted_from: Some(discussion),
495 occurred_at_ms: 100,
496 provenance: None,
497 canonical_body: Default::default(),
498 };
499 let extracted = make(CollaborationOperationBodyV1::Resolve {
500 resolution: CollaborationResolution::IntoContext {
501 context: context.clone(),
502 },
503 })
504 .sign(std::slice::from_ref(&open), &signer)
505 .expect("atomic extraction");
506 let extracted_operation = verify(&extracted).expect("original resolution");
507 extracted_operation
508 .validate_parents(&genesis, &[verify(&open).expect("root")])
509 .expect("discussion causal proof");
510 assert_eq!(
511 extracted_operation
512 .context_revision()
513 .expect("extracted context")
514 .expect("context")
515 .parents,
516 vec![operation_id(&open).expect("root ID")]
517 );
518 context.content = "Refined rationale".into();
519 let revision = sign_context(context.clone(), std::slice::from_ref(&extracted), &signer)
520 .expect("revise directly from original extraction");
521 verify(&revision)
522 .expect("revision")
523 .validate_parents(&genesis, &[extracted_operation])
524 .expect("context retains discussion extraction as causal root");
525 let mut detached = verify(&revision).expect("context operation");
526 let mut detached_context = detached
527 .context_revision()
528 .expect("decode")
529 .expect("context");
530 detached_context.extracted_from = None;
531 detached.body = ThreadOperationBody::Context(detached_context.encode().expect("context"));
532 assert!(
533 detached
534 .validate_parents(
535 &genesis,
536 &[verify(&extracted).expect("original extraction")]
537 )
538 .is_err(),
539 "context revision cannot strip its extraction provenance"
540 );
541 let mut invented_root = verify(&revision).expect("context operation");
542 let mut invented_context = invented_root
543 .context_revision()
544 .expect("decode")
545 .expect("context");
546 invented_context.parents.clear();
547 invented_root.parents.clear();
548 invented_root.body =
549 ThreadOperationBody::Context(invented_context.encode().expect("context"));
550 assert!(
551 invented_root.validate_parents(&genesis, &[]).is_err(),
552 "extraction requires an original signed discussion resolution"
553 );
554 context.metadata.actor.principal_id = Uuid::from_u128(10);
555 assert!(
556 make(CollaborationOperationBodyV1::Resolve {
557 resolution: CollaborationResolution::IntoContext { context }
558 })
559 .sign(&[open], &signer)
560 .is_err(),
561 "extraction cannot assert a different author than the signed resolution"
562 );
563 }
564 #[test]
565 fn canonical_browser_interop_vectors() {
566 use heddle_object_model::object::{CollaborationMention, CollaborationResolution, StateId};
567 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
568 let discussion: DiscussionRecordId = "disc-01980000-0000-7000-8000-000000000123"
569 .parse()
570 .expect("stable discussion ID");
571 let open = command(
572 discussion,
573 CollaborationOperationBodyV1::Open {
574 tags: Vec::new(),
575 blocking: true,
576 title: "Review".into(),
577 anchor: CollaborationAnchor::Repository,
578 visibility: VisibilityTier::Public,
579 turn: DiscussionTurnV1::new("First turn").expect("turn"),
580 thread_ref: None,
581 },
582 )
583 .sign(&[], &signer)
584 .expect("open");
585 let mut append_command = command(discussion, append("See the reviewed state"));
586 append_command.metadata.mentions = vec![CollaborationMention::State {
587 spool: Uuid::from_u128(1),
588 state: StateId::from_bytes([5; 32]),
589 }];
590 let append = append_command
591 .sign(std::slice::from_ref(&open), &signer)
592 .expect("append");
593 let resolve = command(
594 discussion,
595 CollaborationOperationBodyV1::Resolve {
596 resolution: CollaborationResolution::Dismissed {
597 reason: "Verified".into(),
598 },
599 },
600 )
601 .sign(std::slice::from_ref(&append), &signer)
602 .expect("resolve");
603 let reopen = command(
604 discussion,
605 CollaborationOperationBodyV1::Reopen {
606 reason: "New evidence".into(),
607 },
608 )
609 .sign(std::slice::from_ref(&resolve), &signer)
610 .expect("reopen");
611 let context = ContextRevision {
612 version: 2,
613 id: Uuid::from_u128(9),
614 parents: vec![],
615 metadata: command(
616 discussion,
617 CollaborationOperationBodyV1::Reopen {
618 reason: "metadata".into(),
619 },
620 )
621 .metadata,
622 anchor: CollaborationAnchor::Repository,
623 content: "Design rationale".into(),
624 tags: vec!["decision".into()],
625 supersedes: None,
626 extracted_from: Some(discussion),
627 occurred_at_ms: 100,
628 provenance: None,
629 canonical_body: Default::default(),
630 };
631 let extraction = command(
632 discussion,
633 CollaborationOperationBodyV1::Resolve {
634 resolution: CollaborationResolution::IntoContext {
635 context: context.clone(),
636 },
637 },
638 )
639 .sign(std::slice::from_ref(&reopen), &signer)
640 .expect("extraction");
641 let mut revision = context.clone();
642 revision.content = "Refined extracted rationale".into();
643 let extracted_revision = sign_context(revision, std::slice::from_ref(&extraction), &signer)
644 .expect("extracted context revision");
645 let mut standalone_context = context;
646 standalone_context.extracted_from = None;
647 let context = sign_context(standalone_context, &[], &signer).expect("context");
648 let source_record = |revision, target| {
649 command(
650 discussion,
651 CollaborationOperationBodyV1::Open {
652 tags: Vec::new(),
653 blocking: true,
654 title: "Review source".into(),
655 visibility: VisibilityTier::Public,
656 anchor: CollaborationAnchor::Source {
657 source: heddle_object_model::object::CollaborationSourceAnchor {
658 revision,
659 path: "src/main.rs".into(),
660 symbol_id: "run".into(),
661 start_line: Some(12),
662 end_line: Some(18),
663 target,
664 },
665 },
666 turn: DiscussionTurnV1::new("Check these lines").expect("turn"),
667 thread_ref: None,
668 },
669 )
670 .sign(&[], &signer)
671 .expect("source root")
672 };
673 let source_state = source_record(
674 heddle_object_model::object::CollaborationRevision::State {
675 state_id: StateId::from_bytes([5; 32]),
676 },
677 None,
678 );
679 let source_git = source_record(
680 heddle_object_model::object::CollaborationRevision::GitCommit {
681 oid: "a".repeat(40),
682 },
683 None,
684 );
685 let mut structured = verify(&context)
686 .expect("context proof")
687 .context_revision()
688 .expect("decode")
689 .expect("context");
690 structured.tags = vec![
691 "decision".into(),
692 heddle_object_model::object::AnnotationTag::Symbol {
693 name: "authorize".into(),
694 target: Some(heddle_object_model::object::AnnotationSourceReference {
695 scope: structured.metadata.scope.clone(),
696 source: heddle_object_model::object::CollaborationSourceAnchor {
697 revision: heddle_object_model::object::CollaborationRevision::GitCommit {
698 oid: "a".repeat(40),
699 },
700 path: "src/auth.rs".into(),
701 symbol_id: "auth::authorize".into(),
702 start_line: Some(12),
703 end_line: Some(18),
704 target: None,
705 },
706 }),
707 },
708 heddle_object_model::object::AnnotationTag::Property {
709 key: "confidence".into(),
710 value: heddle_object_model::object::AnnotationValue::Decimal(
711 heddle_object_model::object::AnnotationDecimal {
712 coefficient: 9,
713 scale: 1,
714 },
715 ),
716 },
717 heddle_object_model::object::AnnotationTag::Property {
718 key: "requires_review".into(),
719 value: heddle_object_model::object::AnnotationValue::Boolean(false),
720 },
721 heddle_object_model::object::AnnotationTag::Property {
722 key: "large".into(),
723 value: heddle_object_model::object::AnnotationValue::Integer(i64::MAX),
724 },
725 heddle_object_model::object::AnnotationTag::Property {
726 key: "severity".into(),
727 value: heddle_object_model::object::AnnotationValue::Text("high".into()),
728 },
729 ];
730 let structured_context =
731 sign_context(structured, &[], &signer).expect("structured context");
732 use heddle_object_model::object::{
733 AnnotationSourceReference, AnnotationTag, CollaborationRevision, CollaborationScope,
734 CollaborationSourceAnchor,
735 source_target::{SourceTargetBinding, SourceTargetReference},
736 };
737 let mut tracked = Vec::new();
738 for (name, tag_name, binding) in [
739 (
740 "target_viewed",
741 "tag_viewed",
742 SourceTargetBinding::ViewedThread,
743 ),
744 (
745 "target_named",
746 "tag_named",
747 SourceTargetBinding::NamedThread {
748 scope: CollaborationScope {
749 spool: Uuid::from_u128(1),
750 thread: Some(ContentHash::from_bytes([8; 32])),
751 },
752 },
753 ),
754 (
755 "target_pinned",
756 "tag_pinned",
757 SourceTargetBinding::PinnedRevision {
758 scope: CollaborationScope {
759 spool: Uuid::from_u128(1),
760 thread: None,
761 },
762 revision: CollaborationRevision::State {
763 state_id: StateId::from_bytes([5; 32]),
764 },
765 },
766 ),
767 ] {
768 let target = SourceTargetReference {
769 target: ContentHash::from_bytes([6; 32]),
770 binding,
771 };
772 let record = source_record(
773 CollaborationRevision::State {
774 state_id: StateId::from_bytes([5; 32]),
775 },
776 Some(target.clone()),
777 );
778 let mut revision = verify(&context)
779 .expect("context proof")
780 .context_revision()
781 .expect("decode")
782 .expect("context");
783 revision.tags = vec![AnnotationTag::Source {
784 target: AnnotationSourceReference {
785 scope: revision.metadata.scope.clone(),
786 source: CollaborationSourceAnchor {
787 revision: CollaborationRevision::State {
788 state_id: StateId::from_bytes([5; 32]),
789 },
790 path: "src/main.rs".into(),
791 symbol_id: String::new(),
792 start_line: None,
793 end_line: None,
794 target: Some(target),
795 },
796 },
797 }];
798 tracked.push((name, record));
799 tracked.push((
800 tag_name,
801 sign_context(revision, &[], &signer).expect("tracked context"),
802 ));
803 }
804 let mut vectors = String::new();
805 for (name, record) in [
806 ("open", open),
807 ("append", append),
808 ("resolve", resolve),
809 ("reopen", reopen),
810 ("context", context),
811 ("structured_context", structured_context),
812 ("source_state", source_state),
813 ("source_git", source_git),
814 ("extract_context", extraction),
815 ("extracted_revision", extracted_revision),
816 ]
817 .into_iter()
818 .chain(tracked)
819 {
820 vectors.push_str(&format!(
821 "{} {} {} {} {}\n",
822 name,
823 hex::encode(&record.signatures[0].public_key),
824 hex::encode(&record.canonical_record),
825 hex::encode(&record.signatures[0].signature),
826 operation_id(&record).expect("operation ID").to_hex()
827 ));
828 }
829 assert_eq!(
830 vectors,
831 include_str!("../tests/fixtures/collaboration_v2.txt")
832 );
833 }
834}