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
240pub fn sign_context_parent_ids(
244 context: ContextRevision,
245 parent_ids: &[ContentHash],
246 signer: &impl Signer,
247) -> Result<SignedRecord, Error> {
248 if parent_ids.len() > 128 {
249 return Err(Error::Protocol("context has more than 128 causal parents"));
250 }
251 let thread = context.metadata.scope.thread.ok_or(Error::Protocol(
252 "Thread context requires native Thread scope",
253 ))?;
254 if parent_ids.is_empty() && context.extracted_from.is_some() {
255 return Err(Error::Protocol(
256 "context extraction requires a signed discussion resolution",
257 ));
258 }
259 let mut ids = BTreeSet::new();
260 for id in parent_ids {
261 if !ids.insert(*id) {
262 return Err(Error::Protocol("duplicate context causal parent"));
263 }
264 }
265 sign_context_ids(context, thread, ids, signer)
266}
267
268fn sign_context_ids(
269 mut context: ContextRevision,
270 thread: ContentHash,
271 ids: BTreeSet<ContentHash>,
272 signer: &impl Signer,
273) -> Result<SignedRecord, Error> {
274 context.parents = ids.iter().copied().collect();
275 let operation = ThreadOperation {
276 version: 1,
277 thread,
278 parents: ids,
279 publisher: signer
280 .public_key()
281 .try_into()
282 .map_err(|_| Error::Protocol("context requires an Ed25519 signer"))?,
283 body: ThreadOperationBody::Context(
284 context
285 .encode()
286 .map_err(|error| Error::Io(error.to_string()))?,
287 ),
288 };
289 let signed =
290 SignedOperation::sign(&operation, signer).map_err(|error| Error::Io(error.to_string()))?;
291 Ok(SignedRecord {
292 format: OPERATION_FORMAT.into(),
293 canonical_record: signed.canonical,
294 signatures: vec![RecordSignature {
295 public_key: operation.publisher.to_vec(),
296 signature: signed.signature,
297 }],
298 })
299}
300
301pub fn operation_id(record: &SignedRecord) -> Result<ContentHash, Error> {
303 verify(record)?
304 .id()
305 .map_err(|error| Error::Io(error.to_string()))
306}
307
308#[cfg(test)]
309mod tests {
310 use crypto::Ed25519Signer;
311 use heddle_object_model::object::{
312 CollaborationActor, CollaborationAnchor, CollaborationScope, DiscussionTurnV1, Principal,
313 VisibilityTier,
314 };
315 use uuid::Uuid;
316
317 use super::*;
318
319 fn command(discussion: DiscussionRecordId, body: CollaborationOperationBodyV1) -> Command {
320 Command {
321 discussion,
322 operation_id: CollaborationIdempotencyKey::new("operation-1").expect("operation ID"),
323 metadata: CollaborationMetadata {
324 scope: CollaborationScope {
325 spool: Uuid::from_u128(1),
326 thread: Some(ContentHash::from_bytes([2; 32])),
327 },
328 actor: CollaborationActor {
329 principal_id: Uuid::from_u128(3),
330 agent_id: Some("agent-4".into()),
331 },
332 mentions: vec![],
333 },
334 author: Attribution::human(Principal::new("Account", "")),
335 occurred_at_ms: 100,
336 body,
337 }
338 }
339 fn append(body: &str) -> CollaborationOperationBodyV1 {
340 CollaborationOperationBodyV1::AppendTurn {
341 turn: DiscussionTurnV1::new(body).expect("turn"),
342 }
343 }
344 #[test]
345 fn signed_parents_preserve_concurrent_heads_and_reject_changed_proofs() {
346 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
347 let discussion = DiscussionRecordId::generate();
348 let root = command(
349 discussion,
350 CollaborationOperationBodyV1::Open {
351 blocking: false,
352 title: "Review".into(),
353 anchor: CollaborationAnchor::Repository,
354 visibility: VisibilityTier::Private {
355 scope_label: "owner".into(),
356 },
357 turn: DiscussionTurnV1::new("Review this Thread").expect("turn"),
358 thread_ref: None,
359 },
360 )
361 .sign(&[], &signer)
362 .expect("root");
363 let left = command(discussion, append("left"))
364 .sign(std::slice::from_ref(&root), &signer)
365 .expect("left");
366 let right = command(discussion, append("right"))
367 .sign(std::slice::from_ref(&root), &signer)
368 .expect("right");
369 assert_ne!(
370 operation_id(&left).expect("left ID"),
371 operation_id(&right).expect("right ID")
372 );
373 let joined = command(discussion, append("both observed"))
374 .sign(&[left.clone(), right.clone()], &signer)
375 .expect("join");
376 assert_eq!(
377 verify(&joined).expect("verified").parents,
378 BTreeSet::from([
379 operation_id(&left).expect("left ID"),
380 operation_id(&right).expect("right ID")
381 ])
382 );
383 let mut changed = root.clone();
384 changed.signatures[0].signature[0] ^= 1;
385 assert!(
386 command(discussion, append("bad proof"))
387 .sign(&[changed], &signer)
388 .is_err()
389 );
390 assert!(
391 command(DiscussionRecordId::generate(), append("wrong discussion"))
392 .sign(std::slice::from_ref(&root), &signer)
393 .is_err()
394 );
395 assert!(
396 command(discussion, append("duplicate parent"))
397 .sign(&[root.clone(), root], &signer)
398 .is_err()
399 );
400 }
401 #[test]
402 fn observed_parent_ids_are_sorted_unique_and_required_for_append() {
403 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
404 let discussion = DiscussionRecordId::generate();
405 let root = command(
406 discussion,
407 CollaborationOperationBodyV1::Open {
408 blocking: false,
409 title: "Review".into(),
410 anchor: CollaborationAnchor::Repository,
411 visibility: VisibilityTier::Private {
412 scope_label: "owner".into(),
413 },
414 turn: DiscussionTurnV1::new("Review this Thread").expect("turn"),
415 thread_ref: None,
416 },
417 )
418 .sign(&[], &signer)
419 .expect("root");
420 let root_id = operation_id(&root).expect("root ID");
421 let appended = command(discussion, append("follow-up"))
422 .sign_parent_ids(&[root_id], &signer)
423 .expect("append");
424 assert_eq!(
425 verify(&appended).expect("verified").parents,
426 BTreeSet::from([root_id])
427 );
428 assert!(
429 command(discussion, append("no parent"))
430 .sign_parent_ids(&[], &signer)
431 .is_err()
432 );
433 }
434 #[test]
435 fn context_revisions_retain_history_and_bind_parent_record_identity() {
436 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
437 let context = ContextRevision {
438 version: 2,
439 id: Uuid::from_u128(9),
440 parents: vec![],
441 metadata: command(DiscussionRecordId::generate(), append("metadata")).metadata,
442 anchor: CollaborationAnchor::Repository,
443 content: "Original rationale".into(),
444 tags: vec!["decision".into()],
445 supersedes: None,
446 extracted_from: None,
447 occurred_at_ms: 100,
448 provenance: None,
449 canonical_body: Default::default(),
450 };
451 let first = sign_context(context.clone(), &[], &signer).expect("first context");
452 let mut revised = context.clone();
453 revised.content = "Revised rationale".into();
454 let second =
455 sign_context(revised.clone(), std::slice::from_ref(&first), &signer).expect("revision");
456 assert_eq!(
457 verify(&second).expect("second").parents,
458 BTreeSet::from([operation_id(&first).expect("first ID")])
459 );
460 let ThreadOperationBody::Context(bytes) = verify(&first).expect("first").body else {
461 panic!("context");
462 };
463 assert_eq!(
464 ContextRevision::decode(&bytes)
465 .expect("original context")
466 .content,
467 "Original rationale"
468 );
469 revised.id = Uuid::from_u128(10);
470 assert!(
471 sign_context(revised, std::slice::from_ref(&first), &signer).is_err(),
472 "parent cannot silently change record identity"
473 );
474 }
475 #[test]
476 fn extracted_context_keeps_original_resolution_proof_and_actor_binding() {
477 use heddle_object_model::object::{
478 CollaborationResolution, StateId, thread_replication::ThreadGenesis,
479 };
480 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
481 let genesis = ThreadGenesis {
482 version: 1,
483 spool: Uuid::from_u128(1).to_string(),
484 parent: None,
485 base: StateId::from_bytes([3; 32]),
486 name: "extraction".into(),
487 intent: "retain proof".into(),
488 creator: signer.public_key().try_into().expect("key"),
489 owner: heddle_object_model::object::thread_replication::GenesisOwner::LocalKey(
490 signer.public_key().try_into().expect("key"),
491 ),
492 nonce: vec![1; 16],
493 };
494 let discussion = DiscussionRecordId::generate();
495 let make = |body| {
496 let mut command = command(discussion, body);
497 command.metadata.scope.thread = Some(genesis.id().expect("Thread"));
498 command
499 };
500 let open = make(CollaborationOperationBodyV1::Open {
501 blocking: true,
502 title: "Review".into(),
503 anchor: CollaborationAnchor::Repository,
504 visibility: VisibilityTier::Public,
505 turn: DiscussionTurnV1::new("Evidence").expect("turn"),
506 thread_ref: None,
507 })
508 .sign(&[], &signer)
509 .expect("root");
510 let mut context = ContextRevision {
511 version: 2,
512 id: Uuid::from_u128(9),
513 parents: vec![],
514 metadata: make(append("metadata")).metadata,
515 anchor: CollaborationAnchor::Repository,
516 content: "Durable rationale".into(),
517 tags: vec!["decision".into()],
518 supersedes: None,
519 extracted_from: Some(discussion),
520 occurred_at_ms: 100,
521 provenance: None,
522 canonical_body: Default::default(),
523 };
524 let extracted = make(CollaborationOperationBodyV1::Resolve {
525 resolution: CollaborationResolution::IntoContext {
526 context: context.clone(),
527 },
528 })
529 .sign(std::slice::from_ref(&open), &signer)
530 .expect("atomic extraction");
531 let extracted_operation = verify(&extracted).expect("original resolution");
532 extracted_operation
533 .validate_parents(&genesis, &[verify(&open).expect("root")])
534 .expect("discussion causal proof");
535 assert_eq!(
536 extracted_operation
537 .context_revision()
538 .expect("extracted context")
539 .expect("context")
540 .parents,
541 vec![operation_id(&open).expect("root ID")]
542 );
543 context.content = "Refined rationale".into();
544 let revision = sign_context(context.clone(), std::slice::from_ref(&extracted), &signer)
545 .expect("revise directly from original extraction");
546 verify(&revision)
547 .expect("revision")
548 .validate_parents(&genesis, &[extracted_operation])
549 .expect("context retains discussion extraction as causal root");
550 let mut detached = verify(&revision).expect("context operation");
551 let mut detached_context = detached
552 .context_revision()
553 .expect("decode")
554 .expect("context");
555 detached_context.extracted_from = None;
556 detached.body = ThreadOperationBody::Context(detached_context.encode().expect("context"));
557 assert!(
558 detached
559 .validate_parents(
560 &genesis,
561 &[verify(&extracted).expect("original extraction")]
562 )
563 .is_err(),
564 "context revision cannot strip its extraction provenance"
565 );
566 let mut invented_root = verify(&revision).expect("context operation");
567 let mut invented_context = invented_root
568 .context_revision()
569 .expect("decode")
570 .expect("context");
571 invented_context.parents.clear();
572 invented_root.parents.clear();
573 invented_root.body =
574 ThreadOperationBody::Context(invented_context.encode().expect("context"));
575 assert!(
576 invented_root.validate_parents(&genesis, &[]).is_err(),
577 "extraction requires an original signed discussion resolution"
578 );
579 context.metadata.actor.principal_id = Uuid::from_u128(10);
580 assert!(
581 make(CollaborationOperationBodyV1::Resolve {
582 resolution: CollaborationResolution::IntoContext { context }
583 })
584 .sign(&[open], &signer)
585 .is_err(),
586 "extraction cannot assert a different author than the signed resolution"
587 );
588 }
589 #[test]
590 fn canonical_browser_interop_vectors() {
591 use heddle_object_model::object::{CollaborationMention, CollaborationResolution, StateId};
592 let signer = Ed25519Signer::from_seed(&[7; 32]).expect("signer");
593 let discussion: DiscussionRecordId = "disc-01980000-0000-7000-8000-000000000123"
594 .parse()
595 .expect("stable discussion ID");
596 let open = command(
597 discussion,
598 CollaborationOperationBodyV1::Open {
599 blocking: true,
600 title: "Review".into(),
601 anchor: CollaborationAnchor::Repository,
602 visibility: VisibilityTier::Public,
603 turn: DiscussionTurnV1::new("First turn").expect("turn"),
604 thread_ref: None,
605 },
606 )
607 .sign(&[], &signer)
608 .expect("open");
609 let mut append_command = command(discussion, append("See the reviewed state"));
610 append_command.metadata.mentions = vec![CollaborationMention::State {
611 spool: Uuid::from_u128(1),
612 state: StateId::from_bytes([5; 32]),
613 }];
614 let append = append_command
615 .sign(std::slice::from_ref(&open), &signer)
616 .expect("append");
617 let resolve = command(
618 discussion,
619 CollaborationOperationBodyV1::Resolve {
620 resolution: CollaborationResolution::Dismissed {
621 reason: "Verified".into(),
622 },
623 },
624 )
625 .sign(std::slice::from_ref(&append), &signer)
626 .expect("resolve");
627 let reopen = command(
628 discussion,
629 CollaborationOperationBodyV1::Reopen {
630 reason: "New evidence".into(),
631 },
632 )
633 .sign(std::slice::from_ref(&resolve), &signer)
634 .expect("reopen");
635 let context = ContextRevision {
636 version: 2,
637 id: Uuid::from_u128(9),
638 parents: vec![],
639 metadata: command(
640 discussion,
641 CollaborationOperationBodyV1::Reopen {
642 reason: "metadata".into(),
643 },
644 )
645 .metadata,
646 anchor: CollaborationAnchor::Repository,
647 content: "Design rationale".into(),
648 tags: vec!["decision".into()],
649 supersedes: None,
650 extracted_from: Some(discussion),
651 occurred_at_ms: 100,
652 provenance: None,
653 canonical_body: Default::default(),
654 };
655 let extraction = command(
656 discussion,
657 CollaborationOperationBodyV1::Resolve {
658 resolution: CollaborationResolution::IntoContext {
659 context: context.clone(),
660 },
661 },
662 )
663 .sign(std::slice::from_ref(&reopen), &signer)
664 .expect("extraction");
665 let mut revision = context.clone();
666 revision.content = "Refined extracted rationale".into();
667 let extracted_revision = sign_context(revision, std::slice::from_ref(&extraction), &signer)
668 .expect("extracted context revision");
669 let mut standalone_context = context;
670 standalone_context.extracted_from = None;
671 let context = sign_context(standalone_context, &[], &signer).expect("context");
672 let source_record = |revision, target| {
673 command(
674 discussion,
675 CollaborationOperationBodyV1::Open {
676 blocking: true,
677 title: "Review source".into(),
678 visibility: VisibilityTier::Public,
679 anchor: CollaborationAnchor::Source {
680 source: heddle_object_model::object::CollaborationSourceAnchor {
681 revision,
682 path: "src/main.rs".into(),
683 symbol_id: "run".into(),
684 start_line: Some(12),
685 end_line: Some(18),
686 target,
687 },
688 },
689 turn: DiscussionTurnV1::new("Check these lines").expect("turn"),
690 thread_ref: None,
691 },
692 )
693 .sign(&[], &signer)
694 .expect("source root")
695 };
696 let source_state = source_record(
697 heddle_object_model::object::CollaborationRevision::State {
698 state_id: StateId::from_bytes([5; 32]),
699 },
700 None,
701 );
702 let source_git = source_record(
703 heddle_object_model::object::CollaborationRevision::GitCommit {
704 oid: "a".repeat(40),
705 },
706 None,
707 );
708 let mut structured = verify(&context)
709 .expect("context proof")
710 .context_revision()
711 .expect("decode")
712 .expect("context");
713 structured.tags = vec![
714 "decision".into(),
715 heddle_object_model::object::AnnotationTag::Symbol {
716 name: "authorize".into(),
717 target: Some(heddle_object_model::object::AnnotationSourceReference {
718 scope: structured.metadata.scope.clone(),
719 source: heddle_object_model::object::CollaborationSourceAnchor {
720 revision: heddle_object_model::object::CollaborationRevision::GitCommit {
721 oid: "a".repeat(40),
722 },
723 path: "src/auth.rs".into(),
724 symbol_id: "auth::authorize".into(),
725 start_line: Some(12),
726 end_line: Some(18),
727 target: None,
728 },
729 }),
730 },
731 heddle_object_model::object::AnnotationTag::Property {
732 key: "confidence".into(),
733 value: heddle_object_model::object::AnnotationValue::Decimal(
734 heddle_object_model::object::AnnotationDecimal {
735 coefficient: 9,
736 scale: 1,
737 },
738 ),
739 },
740 heddle_object_model::object::AnnotationTag::Property {
741 key: "requires_review".into(),
742 value: heddle_object_model::object::AnnotationValue::Boolean(false),
743 },
744 heddle_object_model::object::AnnotationTag::Property {
745 key: "large".into(),
746 value: heddle_object_model::object::AnnotationValue::Integer(i64::MAX),
747 },
748 heddle_object_model::object::AnnotationTag::Property {
749 key: "severity".into(),
750 value: heddle_object_model::object::AnnotationValue::Text("high".into()),
751 },
752 ];
753 let structured_context =
754 sign_context(structured, &[], &signer).expect("structured context");
755 use heddle_object_model::object::{
756 AnnotationSourceReference, AnnotationTag, CollaborationRevision, CollaborationScope,
757 CollaborationSourceAnchor,
758 source_target::{SourceTargetBinding, SourceTargetReference},
759 };
760 let mut tracked = Vec::new();
761 for (name, tag_name, binding) in [
762 (
763 "target_viewed",
764 "tag_viewed",
765 SourceTargetBinding::ViewedThread,
766 ),
767 (
768 "target_named",
769 "tag_named",
770 SourceTargetBinding::NamedThread {
771 scope: CollaborationScope {
772 spool: Uuid::from_u128(1),
773 thread: Some(ContentHash::from_bytes([8; 32])),
774 },
775 },
776 ),
777 (
778 "target_pinned",
779 "tag_pinned",
780 SourceTargetBinding::PinnedRevision {
781 scope: CollaborationScope {
782 spool: Uuid::from_u128(1),
783 thread: None,
784 },
785 revision: CollaborationRevision::State {
786 state_id: StateId::from_bytes([5; 32]),
787 },
788 },
789 ),
790 ] {
791 let target = SourceTargetReference {
792 target: ContentHash::from_bytes([6; 32]),
793 binding,
794 };
795 let record = source_record(
796 CollaborationRevision::State {
797 state_id: StateId::from_bytes([5; 32]),
798 },
799 Some(target.clone()),
800 );
801 let mut revision = verify(&context)
802 .expect("context proof")
803 .context_revision()
804 .expect("decode")
805 .expect("context");
806 revision.tags = vec![AnnotationTag::Source {
807 target: AnnotationSourceReference {
808 scope: revision.metadata.scope.clone(),
809 source: CollaborationSourceAnchor {
810 revision: CollaborationRevision::State {
811 state_id: StateId::from_bytes([5; 32]),
812 },
813 path: "src/main.rs".into(),
814 symbol_id: String::new(),
815 start_line: None,
816 end_line: None,
817 target: Some(target),
818 },
819 },
820 }];
821 tracked.push((name, record));
822 tracked.push((
823 tag_name,
824 sign_context(revision, &[], &signer).expect("tracked context"),
825 ));
826 }
827 let mut vectors = String::new();
828 for (name, record) in [
829 ("open", open),
830 ("append", append),
831 ("resolve", resolve),
832 ("reopen", reopen),
833 ("context", context),
834 ("structured_context", structured_context),
835 ("source_state", source_state),
836 ("source_git", source_git),
837 ("extract_context", extraction),
838 ("extracted_revision", extracted_revision),
839 ]
840 .into_iter()
841 .chain(tracked)
842 {
843 vectors.push_str(&format!(
844 "{} {} {} {} {}\n",
845 name,
846 hex::encode(&record.signatures[0].public_key),
847 hex::encode(&record.canonical_record),
848 hex::encode(&record.signatures[0].signature),
849 operation_id(&record).expect("operation ID").to_hex()
850 ));
851 }
852 assert_eq!(
853 vectors,
854 include_str!("../tests/fixtures/collaboration_v2.txt")
855 );
856 }
857}