1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::borrow::Cow;
4use std::collections::HashMap;
5
6use crate::knowledge::KnowledgeBase;
7
8pub const SCHEMA_VERSION: u32 = 3;
10
11#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "lowercase")]
14pub enum PatternKind {
15 #[default]
17 Technical,
18 Preference,
20 Fact,
22 Procedure,
24 Behavioral,
26}
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "snake_case")]
31pub enum OriginTrigger {
32 UserExplicit,
34 UserCorrection,
36 AgentInferred,
38 CommunityShared,
40 AutoConsolidated,
42 Automatic,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Origin {
49 pub source: String,
51 pub trigger: OriginTrigger,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub actor: Option<crate::Actor>,
58
59 #[deprecated(note = "use actor instead, removed in v2.3")]
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub user: Option<String>,
63
64 #[deprecated(note = "use actor.source instead, removed in v2.3")]
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub platform: Option<String>,
68
69 #[serde(default = "default_origin_confidence")]
71 pub confidence: f64,
72}
73
74fn default_origin_confidence() -> f64 {
75 1.0
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Pattern {
86 #[serde(flatten)]
88 pub base: KnowledgeBase,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub kind: Option<PatternKind>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub origin: Option<Origin>,
98
99 #[serde(default)]
101 pub attachments: Vec<Attachment>,
102}
103
104impl Pattern {
105 pub fn effective_kind(&self) -> PatternKind {
107 self.kind.unwrap_or(PatternKind::Technical)
108 }
109}
110
111impl std::ops::Deref for Pattern {
113 type Target = KnowledgeBase;
114 fn deref(&self) -> &KnowledgeBase {
115 &self.base
116 }
117}
118impl std::ops::DerefMut for Pattern {
119 fn deref_mut(&mut self) -> &mut KnowledgeBase {
120 &mut self.base
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct Attachment {
127 #[serde(rename = "type")]
129 pub att_type: AttachmentType,
130 pub format: AttachmentFormat,
132 pub path: String,
134 #[serde(default)]
136 pub description: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140#[serde(rename_all = "lowercase")]
141pub enum AttachmentType {
142 Diagram,
143 Image,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147#[serde(rename_all = "lowercase")]
148pub enum AttachmentFormat {
149 Mermaid,
150 #[serde(rename = "plantuml")]
151 PlantUml,
152 Png,
153 Svg,
154}
155
156impl AttachmentFormat {
157 pub fn is_text_based(&self) -> bool {
159 matches!(self, AttachmentFormat::Mermaid | AttachmentFormat::PlantUml)
160 }
161
162 pub fn from_extension(ext: &str) -> Option<Self> {
164 match ext.to_lowercase().as_str() {
165 "mmd" | "mermaid" => Some(AttachmentFormat::Mermaid),
166 "puml" | "plantuml" => Some(AttachmentFormat::PlantUml),
167 "png" => Some(AttachmentFormat::Png),
168 "svg" => Some(AttachmentFormat::Svg),
169 _ => None,
170 }
171 }
172
173 pub fn fence_lang(&self) -> &str {
175 match self {
176 AttachmentFormat::Mermaid => "mermaid",
177 AttachmentFormat::PlantUml => "plantuml",
178 _ => "",
179 }
180 }
181}
182
183impl AttachmentType {
184 pub fn from_format(format: &AttachmentFormat) -> Self {
186 match format {
187 AttachmentFormat::Mermaid | AttachmentFormat::PlantUml => AttachmentType::Diagram,
188 AttachmentFormat::Png | AttachmentFormat::Svg => AttachmentType::Image,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(untagged)]
197pub enum Content {
198 DualLayer {
200 technical: String,
201 #[serde(default)]
202 principle: Option<String>,
203 },
204 Plain(String),
206}
207
208impl Default for Content {
209 fn default() -> Self {
210 Content::Plain(String::new())
211 }
212}
213
214impl Content {
215 pub fn as_text(&self) -> Cow<'_, str> {
220 match self {
221 Content::DualLayer {
222 technical,
223 principle,
224 } => match principle {
225 Some(p) => Cow::Owned(format!("{}\n\n{}", technical, p)),
226 None => Cow::Borrowed(technical),
227 },
228 Content::Plain(s) => Cow::Borrowed(s),
229 }
230 }
231
232 pub const MAX_LAYER_CHARS: usize = 500;
234}
235
236#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
237#[serde(rename_all = "lowercase")]
238pub enum Tier {
239 #[default]
241 Session,
242 Project,
244 Core,
246}
247
248impl Tier {
249 pub fn decay_half_life_days(&self) -> u32 {
251 match self {
252 Tier::Session => 14,
253 Tier::Project => 90,
254 Tier::Core => 365,
255 }
256 }
257}
258
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct Tags {
261 #[serde(default)]
262 pub languages: Vec<String>,
263 #[serde(default)]
264 pub topics: Vec<String>,
265 #[serde(flatten)]
267 pub extra: HashMap<String, Vec<String>>,
268}
269
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
271pub struct Applies {
272 #[serde(default)]
274 pub projects: Vec<String>,
275 #[serde(default)]
276 pub languages: Vec<String>,
277 #[serde(default)]
279 pub tools: Vec<String>,
280 #[serde(default)]
282 pub auto_scope: bool,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct Contribution {
292 #[serde(default)]
293 pub success_signals: u64,
294 #[serde(default)]
295 pub override_signals: u64,
296 pub last_seen: DateTime<Utc>,
297}
298
299#[derive(Debug, Clone, Default, Serialize, Deserialize)]
300pub struct Evidence {
301 #[serde(default)]
302 pub source_sessions: Vec<String>,
303 pub first_seen: Option<DateTime<Utc>>,
304 pub last_validated: Option<DateTime<Utc>>,
305 #[serde(default)]
306 pub injection_count: u64,
307 #[serde(default)]
308 pub success_signals: u64,
309 #[serde(default)]
310 pub failure_signals: u64,
311 #[serde(default)]
312 pub override_signals: u64,
313 #[serde(default)]
316 pub contributions: HashMap<String, Contribution>,
317}
318
319impl Evidence {
320 pub fn effectiveness(&self) -> f64 {
322 let total = self.success_signals + self.override_signals;
323 if total == 0 {
324 0.5 } else {
326 self.success_signals as f64 / total as f64
327 }
328 }
329
330 pub fn effectiveness_by_actor(&self, actor: &crate::Actor) -> f64 {
336 match self.contributions.get(&actor.key()) {
337 Some(c) => {
338 let total = c.success_signals + c.override_signals;
339 if total == 0 {
340 0.5 } else {
342 c.success_signals as f64 / total as f64
343 }
344 }
345 None => 0.5, }
347 }
348}
349
350#[derive(Debug, Clone, Default, Serialize, Deserialize)]
351pub struct Links {
352 #[serde(default)]
354 pub related: Vec<String>,
355 #[serde(default)]
357 pub supersedes: Vec<String>,
358 #[serde(default)]
360 pub workflows: Vec<String>,
361}
362
363#[derive(Debug, Clone, Default, Serialize, Deserialize)]
364pub struct Lifecycle {
365 #[serde(default)]
366 pub status: LifecycleStatus,
367 pub decay_half_life: Option<u32>,
369 pub last_injected: Option<DateTime<Utc>>,
370 #[serde(default)]
372 pub pinned: bool,
373 #[serde(default)]
375 pub muted: bool,
376}
377
378#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
379#[serde(rename_all = "lowercase")]
380pub enum LifecycleStatus {
381 #[default]
382 Active,
383 Deprecated,
384 Archived,
385}
386
387pub fn default_schema() -> u32 {
388 SCHEMA_VERSION
389}
390pub fn default_importance() -> f64 {
391 0.5
392}
393pub fn default_confidence() -> f64 {
394 0.5
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_attachment_format_is_text_based() {
403 assert!(AttachmentFormat::Mermaid.is_text_based());
404 assert!(AttachmentFormat::PlantUml.is_text_based());
405 assert!(!AttachmentFormat::Png.is_text_based());
406 assert!(!AttachmentFormat::Svg.is_text_based());
407 }
408
409 #[test]
410 fn test_attachment_format_from_extension() {
411 assert_eq!(
412 AttachmentFormat::from_extension("mmd"),
413 Some(AttachmentFormat::Mermaid)
414 );
415 assert_eq!(
416 AttachmentFormat::from_extension("mermaid"),
417 Some(AttachmentFormat::Mermaid)
418 );
419 assert_eq!(
420 AttachmentFormat::from_extension("puml"),
421 Some(AttachmentFormat::PlantUml)
422 );
423 assert_eq!(
424 AttachmentFormat::from_extension("plantuml"),
425 Some(AttachmentFormat::PlantUml)
426 );
427 assert_eq!(
428 AttachmentFormat::from_extension("png"),
429 Some(AttachmentFormat::Png)
430 );
431 assert_eq!(
432 AttachmentFormat::from_extension("svg"),
433 Some(AttachmentFormat::Svg)
434 );
435 assert_eq!(AttachmentFormat::from_extension("jpg"), None);
436 assert_eq!(AttachmentFormat::from_extension(""), None);
437 assert_eq!(
439 AttachmentFormat::from_extension("MMD"),
440 Some(AttachmentFormat::Mermaid)
441 );
442 }
443
444 #[test]
445 fn test_attachment_format_fence_lang() {
446 assert_eq!(AttachmentFormat::Mermaid.fence_lang(), "mermaid");
447 assert_eq!(AttachmentFormat::PlantUml.fence_lang(), "plantuml");
448 assert_eq!(AttachmentFormat::Png.fence_lang(), "");
449 }
450
451 #[test]
452 fn test_attachment_type_from_format() {
453 assert_eq!(
454 AttachmentType::from_format(&AttachmentFormat::Mermaid),
455 AttachmentType::Diagram
456 );
457 assert_eq!(
458 AttachmentType::from_format(&AttachmentFormat::PlantUml),
459 AttachmentType::Diagram
460 );
461 assert_eq!(
462 AttachmentType::from_format(&AttachmentFormat::Png),
463 AttachmentType::Image
464 );
465 assert_eq!(
466 AttachmentType::from_format(&AttachmentFormat::Svg),
467 AttachmentType::Image
468 );
469 }
470
471 #[test]
472 fn test_attachment_serde() {
473 let att = Attachment {
474 att_type: AttachmentType::Diagram,
475 format: AttachmentFormat::Mermaid,
476 path: "my-pattern/arch.mermaid".to_string(),
477 description: "Architecture diagram".to_string(),
478 };
479
480 let yaml = serde_yaml::to_string(&att).unwrap();
481 assert!(yaml.contains("type: diagram"));
482 assert!(yaml.contains("format: mermaid"));
483 assert!(yaml.contains("path: my-pattern/arch.mermaid"));
484 assert!(yaml.contains("description: Architecture diagram"));
485
486 let deserialized: Attachment = serde_yaml::from_str(&yaml).unwrap();
487 assert_eq!(deserialized.att_type, AttachmentType::Diagram);
488 assert_eq!(deserialized.format, AttachmentFormat::Mermaid);
489 }
490
491 #[test]
492 fn test_attachment_svg_serde() {
493 let att = Attachment {
494 att_type: AttachmentType::Image,
495 format: AttachmentFormat::Svg,
496 path: "my-pattern/logo.svg".to_string(),
497 description: "Logo".to_string(),
498 };
499
500 let yaml = serde_yaml::to_string(&att).unwrap();
501 let deserialized: Attachment = serde_yaml::from_str(&yaml).unwrap();
502 assert_eq!(deserialized.format, AttachmentFormat::Svg);
503 assert_eq!(deserialized.att_type, AttachmentType::Image);
504 }
505
506 #[test]
507 fn test_pattern_kind_serde_roundtrip() {
508 let cases = vec![
510 (PatternKind::Technical, "technical"),
511 (PatternKind::Preference, "preference"),
512 (PatternKind::Fact, "fact"),
513 (PatternKind::Procedure, "procedure"),
514 (PatternKind::Behavioral, "behavioral"),
515 ];
516 for (kind, expected_str) in cases {
517 let yaml = serde_yaml::to_string(&kind).unwrap();
518 assert!(
519 yaml.contains(expected_str),
520 "Expected '{}' in '{}'",
521 expected_str,
522 yaml
523 );
524 let deserialized: PatternKind = serde_yaml::from_str(&yaml).unwrap();
525 assert_eq!(deserialized, kind);
526 }
527 }
528
529 #[test]
530 fn test_origin_trigger_serde_roundtrip() {
531 let cases = vec![
532 (OriginTrigger::UserExplicit, "user_explicit"),
533 (OriginTrigger::UserCorrection, "user_correction"),
534 (OriginTrigger::AgentInferred, "agent_inferred"),
535 (OriginTrigger::CommunityShared, "community_shared"),
536 (OriginTrigger::AutoConsolidated, "auto_consolidated"),
537 ];
538 for (trigger, expected_str) in cases {
539 let yaml = serde_yaml::to_string(&trigger).unwrap();
540 assert!(yaml.contains(expected_str));
541 let deserialized: OriginTrigger = serde_yaml::from_str(&yaml).unwrap();
542 assert_eq!(deserialized, trigger);
543 }
544 }
545
546 #[test]
547 fn test_origin_serde_roundtrip() {
548 #[allow(deprecated)] let origin = Origin {
550 source: "commander".to_string(),
551 trigger: OriginTrigger::UserExplicit,
552 actor: None,
553 user: Some("david".to_string()),
554 platform: Some("slack".to_string()),
555 confidence: 0.95,
556 };
557 let yaml = serde_yaml::to_string(&origin).unwrap();
558 assert!(yaml.contains("source: commander"));
559 assert!(yaml.contains("trigger: user_explicit"));
560 assert!(yaml.contains("user: david"));
561 assert!(yaml.contains("platform: slack"));
562
563 let deserialized: Origin = serde_yaml::from_str(&yaml).unwrap();
564 assert_eq!(deserialized.source, "commander");
565 assert_eq!(deserialized.trigger, OriginTrigger::UserExplicit);
566 #[allow(deprecated)]
567 {
568 assert_eq!(deserialized.user, Some("david".to_string()));
569 assert_eq!(deserialized.platform, Some("slack".to_string()));
570 }
571 assert!((deserialized.confidence - 0.95).abs() < 0.001);
572 }
573
574 #[test]
575 fn test_origin_optional_fields_omitted() {
576 #[allow(deprecated)] let origin = Origin {
578 source: "cli".to_string(),
579 trigger: OriginTrigger::AgentInferred,
580 actor: None,
581 user: None,
582 platform: None,
583 confidence: 1.0,
584 };
585 let yaml = serde_yaml::to_string(&origin).unwrap();
586 assert!(!yaml.contains("user:"));
587 assert!(!yaml.contains("platform:"));
588 }
589
590 #[test]
591 fn test_pattern_with_kind_and_origin_roundtrip() {
592 use crate::knowledge::KnowledgeBase;
593 let pattern = Pattern {
594 base: KnowledgeBase {
595 name: "test-pref".into(),
596 description: "A preference".into(),
597 content: Content::Plain("Use Chinese".into()),
598 ..Default::default()
599 },
600 kind: Some(PatternKind::Preference),
601 #[allow(deprecated)] origin: Some(Origin {
603 source: "commander".into(),
604 trigger: OriginTrigger::UserExplicit,
605 actor: None,
606 user: Some("david".into()),
607 platform: None,
608 confidence: 0.9,
609 }),
610 attachments: vec![],
611 };
612
613 let yaml = serde_yaml::to_string(&pattern).unwrap();
614 assert!(yaml.contains("kind: preference"));
615 assert!(yaml.contains("source: commander"));
616
617 let deserialized: Pattern = serde_yaml::from_str(&yaml).unwrap();
618 assert_eq!(deserialized.kind, Some(PatternKind::Preference));
619 assert_eq!(deserialized.effective_kind(), PatternKind::Preference);
620 assert!(deserialized.origin.is_some());
621 assert_eq!(deserialized.origin.unwrap().source, "commander");
622 }
623
624 #[test]
625 fn origin_with_actor_roundtrip() {
626 use crate::{Actor, ActorSource};
627 #[allow(deprecated)]
628 let o = Origin {
629 source: "commander".into(),
630 trigger: OriginTrigger::AgentInferred,
631 actor: Some(Actor {
632 source: ActorSource::Slack,
633 native_id: "U999".into(),
634 display_name: Some("bob".into()),
635 resolved_user_id: None,
636 }),
637 user: None,
638 platform: None,
639 confidence: 0.8,
640 };
641 let y = serde_yaml::to_string(&o).unwrap();
642 let back: Origin = serde_yaml::from_str(&y).unwrap();
643 assert_eq!(back.actor.as_ref().unwrap().native_id, "U999");
644 }
645
646 #[test]
647 fn origin_backward_compat_no_actor_field() {
648 let old_yaml = r#"
650source: starter
651trigger: automatic
652confidence: 0.5
653"#;
654 let o: Origin = serde_yaml::from_str(old_yaml).unwrap();
655 assert!(o.actor.is_none());
656 #[allow(deprecated)]
657 {
658 assert!(o.user.is_none());
659 assert!(o.platform.is_none());
660 }
661 }
662
663 #[test]
664 fn origin_reads_legacy_user_platform_yaml() {
665 let legacy_yaml = r#"
667source: import
668trigger: automatic
669user: alice
670platform: "CLAUDE.md"
671confidence: 0.7
672"#;
673 let o: Origin = serde_yaml::from_str(legacy_yaml).unwrap();
674 #[allow(deprecated)]
675 {
676 assert_eq!(o.user.as_deref(), Some("alice"));
677 assert_eq!(o.platform.as_deref(), Some("CLAUDE.md"));
678 }
679 assert!(o.actor.is_none());
680 }
681
682 #[test]
683 fn test_pattern_backward_compat_no_kind_no_origin() {
684 let yaml = "name: old-pattern\ndescription: Old\ncontent: Some content\n";
686 let pattern: Pattern = serde_yaml::from_str(yaml).unwrap();
687 assert_eq!(pattern.name, "old-pattern");
688 assert!(pattern.kind.is_none());
689 assert!(pattern.origin.is_none());
690 assert_eq!(pattern.effective_kind(), PatternKind::Technical);
691 }
692
693 #[test]
694 fn test_pattern_kind_default() {
695 assert_eq!(PatternKind::default(), PatternKind::Technical);
696 }
697
698 #[test]
699 fn evidence_contributions_default_empty() {
700 let e = Evidence::default();
701 assert!(e.contributions.is_empty());
702 }
703
704 #[test]
705 fn evidence_effectiveness_by_actor_splits_signals() {
706 use crate::{Actor, ActorSource};
707
708 let alice = Actor {
709 source: ActorSource::Slack,
710 native_id: "alice".into(),
711 display_name: None,
712 resolved_user_id: None,
713 };
714 let bob = Actor {
715 source: ActorSource::Slack,
716 native_id: "bob".into(),
717 display_name: None,
718 resolved_user_id: None,
719 };
720
721 let mut contribs = HashMap::new();
722 contribs.insert(
723 alice.key(),
724 Contribution {
725 success_signals: 8,
726 override_signals: 2,
727 last_seen: Utc::now(),
728 },
729 );
730 contribs.insert(
731 bob.key(),
732 Contribution {
733 success_signals: 1,
734 override_signals: 4,
735 last_seen: Utc::now(),
736 },
737 );
738
739 let e = Evidence {
740 source_sessions: vec![],
741 first_seen: None,
742 last_validated: None,
743 injection_count: 15,
744 success_signals: 9,
745 failure_signals: 0,
746 override_signals: 6,
747 contributions: contribs,
748 };
749
750 assert!((e.effectiveness_by_actor(&alice) - 0.8).abs() < 0.001);
751 assert!((e.effectiveness_by_actor(&bob) - 0.2).abs() < 0.001);
752 assert!((e.effectiveness() - 0.6).abs() < 0.001);
755 }
756
757 #[test]
758 fn evidence_effectiveness_by_unknown_actor_returns_neutral_prior() {
759 use crate::{Actor, ActorSource};
760
761 let unknown = Actor {
762 source: ActorSource::MurCli,
763 native_id: "never-seen".into(),
764 display_name: None,
765 resolved_user_id: None,
766 };
767 let e = Evidence::default();
768 assert!((e.effectiveness_by_actor(&unknown) - 0.5).abs() < 0.001);
770 }
771
772 #[test]
773 fn evidence_yaml_roundtrip_with_contributions() {
774 use crate::{Actor, ActorSource};
775
776 let actor = Actor {
777 source: ActorSource::CommanderDaemon,
778 native_id: "svc-1".into(),
779 display_name: None,
780 resolved_user_id: None,
781 };
782 let mut contribs = HashMap::new();
783 contribs.insert(
784 actor.key(),
785 Contribution {
786 success_signals: 3,
787 override_signals: 1,
788 last_seen: DateTime::parse_from_rfc3339("2026-04-18T10:00:00Z")
789 .unwrap()
790 .with_timezone(&Utc),
791 },
792 );
793 let e = Evidence {
794 contributions: contribs,
795 ..Evidence::default()
796 };
797 let y = serde_yaml::to_string(&e).unwrap();
798 let back: Evidence = serde_yaml::from_str(&y).unwrap();
799 assert_eq!(back.contributions.len(), 1);
800 assert_eq!(
801 back.contributions
802 .get("CommanderDaemon:svc-1")
803 .unwrap()
804 .success_signals,
805 3
806 );
807 }
808
809 #[test]
810 fn evidence_yaml_backward_compat_no_contributions_field() {
811 let old_yaml = r#"
813source_sessions: []
814first_seen: null
815last_validated: null
816injection_count: 5
817success_signals: 3
818override_signals: 1
819"#;
820 let e: Evidence = serde_yaml::from_str(old_yaml).unwrap();
821 assert!(e.contributions.is_empty());
822 assert_eq!(e.success_signals, 3);
823 }
824
825 #[test]
826 fn pattern_scope_defaults_personal() {
827 let old_yaml = r#"
829schema: 2
830name: legacy-pattern
831description: legacy
832content: old content
833tier: session
834"#;
835 let p: Pattern = serde_yaml::from_str(old_yaml).unwrap();
836 assert_eq!(p.scope, crate::Scope::Personal);
837 }
838
839 #[test]
840 fn pattern_scope_team_roundtrip() {
841 let y = r#"
842schema: 2
843name: team-pat
844description: team pattern
845content: team content
846tier: project
847scope:
848 kind: team
849 team_id: ops
850"#;
851 let p: Pattern = serde_yaml::from_str(y).unwrap();
852 assert_eq!(
853 p.scope,
854 crate::Scope::Team {
855 team_id: "ops".into()
856 }
857 );
858 let y2 = serde_yaml::to_string(&p).unwrap();
860 let p2: Pattern = serde_yaml::from_str(&y2).unwrap();
861 assert_eq!(p2.scope, p.scope);
862 }
863
864 #[test]
865 fn pattern_scope_community_roundtrip() {
866 let y = r#"
867schema: 2
868name: comm-pat
869description: community pattern
870content: community content
871tier: core
872scope:
873 kind: community
874 pack_id: rust-best-practices
875"#;
876 let p: Pattern = serde_yaml::from_str(y).unwrap();
877 assert_eq!(
878 p.scope,
879 crate::Scope::Community {
880 pack_id: Some("rust-best-practices".into())
881 }
882 );
883 }
884}