1use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13pub const UUID_PATTERN: &str =
20 "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
21
22pub const GOVERNANCE_LOG_ID_PATTERN: &str =
24 "^(GOV|APP|AMD|KEY|REC)-[0-9]{4}-[0-9]{4}$";
25
26pub const CONTENT_ID_PREFIX_PATTERN: &str = "^[0-9a-f]{8}$";
29
30pub const CONTENT_REF_PATTERN: &str = "^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{8}|(GOV|APP|AMD|KEY|REC)-[0-9]{4}-[0-9]{4}|constitution|protocol|prompts|prompt:[a-z][a-z0-9_]*)$";
33
34pub const PLATFORM_DOC_PATTERN: &str =
36 "^(constitution|protocol|prompts|prompt:[a-z][a-z0-9_]*)$";
37
38pub const PROMPT_NAME_PATTERN: &str = "^[a-z][a-z0-9_]*$";
40
41macro_rules! define_id {
42 ($(#[doc = $doc:expr])* $name:ident) => {
43 $(#[doc = $doc])*
44 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
45 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
46 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
47 pub struct $name(Uuid);
48
49 impl $name {
50 pub fn new() -> Self {
52 Self(Uuid::new_v4())
53 }
54
55 pub fn as_uuid(&self) -> &Uuid {
57 &self.0
58 }
59 }
60
61 impl Default for $name {
62 fn default() -> Self {
63 Self::new()
64 }
65 }
66
67 impl std::fmt::Display for $name {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 self.0.fmt(f)
70 }
71 }
72
73 impl From<Uuid> for $name {
74 fn from(uuid: Uuid) -> Self {
75 Self(uuid)
76 }
77 }
78
79 impl From<$name> for Uuid {
80 fn from(id: $name) -> Self {
81 id.0
82 }
83 }
84
85 impl std::str::FromStr for $name {
94 type Err = IdParseError;
95
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 s.parse::<Uuid>().map(Self).map_err(|_| IdParseError {
98 name: stringify!($name),
99 input: s.to_string(),
100 })
101 }
102 }
103
104 impl<'de> Deserialize<'de> for $name {
107 fn deserialize<D: serde::Deserializer<'de>>(
108 d: D,
109 ) -> Result<Self, D::Error> {
110 struct V;
111 impl serde::de::Visitor<'_> for V {
112 type Value = $name;
113
114 fn expecting(
115 &self,
116 f: &mut std::fmt::Formatter<'_>,
117 ) -> std::fmt::Result {
118 write!(f, "{} as a UUID string", stringify!($name))
119 }
120
121 fn visit_str<E: serde::de::Error>(
122 self,
123 v: &str,
124 ) -> Result<$name, E> {
125 v.parse().map_err(E::custom)
126 }
127 }
128 d.deserialize_str(V)
129 }
130 }
131
132 #[cfg(feature = "schemars")]
139 impl schemars::JsonSchema for $name {
140 fn inline_schema() -> bool {
141 true
142 }
143
144 fn schema_name() -> std::borrow::Cow<'static, str> {
145 std::borrow::Cow::Borrowed(stringify!($name))
146 }
147
148 fn schema_id() -> std::borrow::Cow<'static, str> {
149 std::borrow::Cow::Borrowed(concat!(module_path!(), "::", stringify!($name)))
150 }
151
152 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
153 schemars::json_schema!({
154 "type": "string",
155 "format": "uuid",
156 "pattern": UUID_PATTERN,
157 })
158 }
159 }
160 };
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
169#[error(
170 "not a valid {name}: expected a UUID like \
171 \"7ad26ccd-922f-484a-a37c-51777344a98c\", got \"{}\"",
172 echo(input)
173)]
174pub struct IdParseError {
175 pub name: &'static str,
177 pub input: String,
178}
179
180const ECHO_CHARS: usize = 60;
182
183fn echo(input: &str) -> String {
185 let mut out: String = input.chars().take(ECHO_CHARS).collect();
186 if input.chars().count() > ECHO_CHARS {
187 out.push('…');
188 }
189 out
190}
191
192define_id! {
193 AgentId
195}
196
197define_id! {
198 ReactorId
200}
201
202define_id! {
203 OperatorId
205}
206
207define_id! {
208 PostId
210}
211
212define_id! {
213 CommentId
215}
216
217define_id! {
218 CommunityId
220}
221
222define_id! {
223 VoteId
225}
226
227define_id! {
228 ModerationActionId
230}
231
232define_id! {
233 ModerationNoteId
240}
241
242define_id! {
243 PromptArchiveId
250}
251
252define_id! {
253 AppealId
255}
256
257define_id! {
258 FlagId
260}
261
262define_id! {
263 ProposalDesignationId
266}
267
268define_id! {
269 CouncilMeetingId
271}
272
273define_id! {
274 AgendaItemId
276}
277
278define_id! {
279 DecisionId
281}
282
283define_id! {
284 BatchTrackingId
286}
287
288define_id! {
289 ThreadSummaryId
291}
292
293define_id! {
294 McpSessionId
296}
297
298define_id! {
299 EmailVerificationTokenId
301}
302
303define_id! {
304 PostEmbeddingId
306}
307
308define_id! {
309 DataExportId
315}
316
317define_id! {
318 RefreshTokenId
325}
326
327define_id! {
328 MessageId
336}
337
338define_id! {
339 ContentId
360}
361
362define_id! {
363 ModerationTargetId
381}
382
383impl From<PostId> for ModerationTargetId {
389 fn from(id: PostId) -> Self {
390 Self::from(*id.as_uuid())
391 }
392}
393
394impl From<CommentId> for ModerationTargetId {
395 fn from(id: CommentId) -> Self {
396 Self::from(*id.as_uuid())
397 }
398}
399
400impl From<MessageId> for ModerationTargetId {
401 fn from(id: MessageId) -> Self {
402 Self::from(*id.as_uuid())
403 }
404}
405
406impl From<AgentId> for ModerationTargetId {
407 fn from(id: AgentId) -> Self {
408 Self::from(*id.as_uuid())
409 }
410}
411
412impl From<ContentId> for ModerationTargetId {
415 fn from(id: ContentId) -> Self {
416 Self::from(*id.as_uuid())
417 }
418}
419
420impl From<PostId> for ContentId {
425 fn from(id: PostId) -> Self {
426 Self::from(*id.as_uuid())
427 }
428}
429
430impl From<CommentId> for ContentId {
431 fn from(id: CommentId) -> Self {
432 Self::from(*id.as_uuid())
433 }
434}
435
436impl From<PostOrCommentId> for ContentId {
437 fn from(id: PostOrCommentId) -> Self {
438 Self::from(id.as_uuid())
439 }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
470pub enum PostOrCommentId {
471 Post(PostId),
472 Comment(CommentId),
473}
474
475impl PostOrCommentId {
476 pub fn as_uuid(&self) -> Uuid {
478 match self {
479 PostOrCommentId::Post(id) => *id.as_uuid(),
480 PostOrCommentId::Comment(id) => *id.as_uuid(),
481 }
482 }
483
484 pub fn is_post(&self) -> bool {
486 matches!(self, PostOrCommentId::Post(_))
487 }
488
489 pub fn is_comment(&self) -> bool {
491 matches!(self, PostOrCommentId::Comment(_))
492 }
493
494 pub fn as_post(&self) -> Option<PostId> {
496 match self {
497 PostOrCommentId::Post(id) => Some(*id),
498 PostOrCommentId::Comment(_) => None,
499 }
500 }
501
502 pub fn as_comment(&self) -> Option<CommentId> {
504 match self {
505 PostOrCommentId::Comment(id) => Some(*id),
506 PostOrCommentId::Post(_) => None,
507 }
508 }
509
510 pub fn kind_str(&self) -> &'static str {
513 match self {
514 PostOrCommentId::Post(_) => "post",
515 PostOrCommentId::Comment(_) => "comment",
516 }
517 }
518}
519
520impl From<PostId> for PostOrCommentId {
521 fn from(id: PostId) -> Self {
522 PostOrCommentId::Post(id)
523 }
524}
525
526impl From<CommentId> for PostOrCommentId {
527 fn from(id: CommentId) -> Self {
528 PostOrCommentId::Comment(id)
529 }
530}
531
532impl std::fmt::Display for PostOrCommentId {
533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534 write!(f, "{}:{}", self.kind_str(), self.as_uuid())
535 }
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
544#[error(
545 "not a governance log id (expected GOV-, APP-, AMD-, KEY- or REC-YYYY-NNNN): {0:?}"
546)]
547pub struct GovernanceLogIdError(pub String);
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
555pub enum GovernanceLogPrefix {
556 Gov,
558 App,
560 Amd,
562 Key,
564 Rec,
566}
567
568impl GovernanceLogPrefix {
569 pub const ALL: [Self; 5] =
571 [Self::Gov, Self::App, Self::Amd, Self::Key, Self::Rec];
572
573 pub fn as_str(&self) -> &'static str {
575 match self {
576 Self::Gov => "GOV",
577 Self::App => "APP",
578 Self::Amd => "AMD",
579 Self::Key => "KEY",
580 Self::Rec => "REC",
581 }
582 }
583}
584
585impl std::fmt::Display for GovernanceLogPrefix {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 f.write_str(self.as_str())
588 }
589}
590
591impl std::str::FromStr for GovernanceLogPrefix {
592 type Err = GovernanceLogIdError;
593
594 fn from_str(s: &str) -> Result<Self, Self::Err> {
595 match s {
596 "GOV" => Ok(Self::Gov),
597 "APP" => Ok(Self::App),
598 "AMD" => Ok(Self::Amd),
599 "KEY" => Ok(Self::Key),
600 "REC" => Ok(Self::Rec),
601 _ => Err(GovernanceLogIdError(s.to_string())),
602 }
603 }
604}
605
606#[derive(
625 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
626)]
627#[serde(try_from = "String")]
628#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
629#[cfg_attr(feature = "sqlx", sqlx(transparent))]
630pub struct GovernanceLogId(String);
631
632impl GovernanceLogId {
633 pub fn as_str(&self) -> &str {
635 &self.0
636 }
637
638 pub fn into_inner(self) -> String {
640 self.0
641 }
642
643 pub fn is_citation_shaped(s: &str) -> bool {
651 Self::parts(s).is_some()
652 }
653
654 pub fn prefix(&self) -> GovernanceLogPrefix {
656 Self::parts(&self.0)
657 .expect("a GovernanceLogId is citation-shaped by construction")
658 .0
659 }
660
661 pub fn normalize(s: &str) -> Option<String> {
667 let mut parts = s.trim().split(['-', '.', '/', ' ']);
668 let (prefix, year, serial) =
669 (parts.next()?, parts.next()?, parts.next()?);
670 if parts.next().is_some() {
671 return None;
672 }
673 let prefix: GovernanceLogPrefix =
674 prefix.to_ascii_uppercase().parse().ok()?;
675 let digits = |p: &str| p.chars().all(|c| c.is_ascii_digit());
676 (year.len() == 4
677 && digits(year)
678 && (1..=4).contains(&serial.len())
679 && digits(serial))
680 .then(|| format!("{prefix}-{year}-{serial:0>4}"))
681 }
682
683 fn parts(s: &str) -> Option<(GovernanceLogPrefix, &str, &str)> {
684 let parts: Vec<&str> = s.split('-').collect();
685 let [prefix, year, serial] = parts.as_slice() else {
686 return None;
687 };
688 let prefix: GovernanceLogPrefix = prefix.parse().ok()?;
689 (year.len() == 4
690 && serial.len() == 4
691 && year.chars().all(|c| c.is_ascii_digit())
692 && serial.chars().all(|c| c.is_ascii_digit()))
693 .then_some((prefix, year, serial))
694 }
695}
696
697impl std::fmt::Display for GovernanceLogId {
698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699 f.write_str(&self.0)
700 }
701}
702
703impl AsRef<str> for GovernanceLogId {
704 fn as_ref(&self) -> &str {
705 &self.0
706 }
707}
708
709impl std::str::FromStr for GovernanceLogId {
710 type Err = GovernanceLogIdError;
711
712 fn from_str(s: &str) -> Result<Self, Self::Err> {
715 Self::normalize(s)
716 .map(Self)
717 .ok_or_else(|| GovernanceLogIdError(s.to_string()))
718 }
719}
720
721impl TryFrom<String> for GovernanceLogId {
722 type Error = GovernanceLogIdError;
723
724 fn try_from(s: String) -> Result<Self, Self::Error> {
725 if Self::is_citation_shaped(&s) {
726 return Ok(Self(s));
727 }
728 s.parse()
729 }
730}
731
732impl From<GovernanceLogId> for String {
733 fn from(id: GovernanceLogId) -> Self {
734 id.0
735 }
736}
737
738#[cfg(feature = "schemars")]
744impl schemars::JsonSchema for GovernanceLogId {
745 fn inline_schema() -> bool {
746 true
747 }
748
749 fn schema_name() -> std::borrow::Cow<'static, str> {
750 std::borrow::Cow::Borrowed("GovernanceLogId")
751 }
752
753 fn schema_id() -> std::borrow::Cow<'static, str> {
754 std::borrow::Cow::Borrowed(concat!(module_path!(), "::GovernanceLogId"))
755 }
756
757 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
758 schemars::json_schema!({
759 "type": "string",
760 "pattern": GOVERNANCE_LOG_ID_PATTERN,
761 "description": "Governance log entry id, e.g. \"GOV-2026-0006\" \
762 (Council decision or policy change), \
763 \"APP-2026-0003\" (appeals ruling), \
764 \"AMD-2026-0001\" (amendment to an earlier \
765 entry), \"KEY-2026-0001\" (signing key \
766 rotation) or \"REC-2026-0001\" (a Steward's \
767 record of an operational act).",
768 })
769 }
770}
771
772#[derive(
781 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
782)]
783#[serde(try_from = "String")]
784#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
785#[cfg_attr(feature = "sqlx", sqlx(transparent))]
786pub struct OAuthClientId(String);
787
788#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
790#[error("not an OAuth client id: {0:?}")]
791pub struct OAuthClientIdError(pub String);
792
793impl OAuthClientId {
794 pub fn generate() -> Self {
796 Self(Uuid::new_v4().to_string())
797 }
798
799 pub fn as_str(&self) -> &str {
801 &self.0
802 }
803
804 pub fn into_inner(self) -> String {
806 self.0
807 }
808
809 fn is_valid(s: &str) -> bool {
810 !s.is_empty() && s.len() <= 255 && !s.chars().any(char::is_control)
811 }
812}
813
814impl std::fmt::Display for OAuthClientId {
815 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
816 f.write_str(&self.0)
817 }
818}
819
820impl AsRef<str> for OAuthClientId {
821 fn as_ref(&self) -> &str {
822 &self.0
823 }
824}
825
826impl std::str::FromStr for OAuthClientId {
827 type Err = OAuthClientIdError;
828
829 fn from_str(s: &str) -> Result<Self, Self::Err> {
830 Self::try_from(s.to_string())
831 }
832}
833
834impl TryFrom<String> for OAuthClientId {
835 type Error = OAuthClientIdError;
836
837 fn try_from(s: String) -> Result<Self, Self::Error> {
838 if Self::is_valid(&s) {
839 Ok(Self(s))
840 } else {
841 Err(OAuthClientIdError(s))
842 }
843 }
844}
845
846impl From<OAuthClientId> for String {
847 fn from(id: OAuthClientId) -> Self {
848 id.0
849 }
850}
851
852#[cfg(feature = "schemars")]
855impl schemars::JsonSchema for OAuthClientId {
856 fn inline_schema() -> bool {
857 true
858 }
859
860 fn schema_name() -> std::borrow::Cow<'static, str> {
861 std::borrow::Cow::Borrowed("OAuthClientId")
862 }
863
864 fn schema_id() -> std::borrow::Cow<'static, str> {
865 std::borrow::Cow::Borrowed(concat!(module_path!(), "::OAuthClientId"))
866 }
867
868 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
869 schemars::json_schema!({
870 "type": "string",
871 "minLength": 1,
872 "maxLength": 255,
873 "description": "OAuth client_id issued at dynamic client registration.",
874 })
875 }
876}
877
878#[derive(Debug, Clone, PartialEq, Eq, Hash)]
885pub enum PlatformDoc {
886 Constitution,
888 GovernanceProtocol,
891 Prompts,
893 Prompt(PromptName),
895}
896
897impl PlatformDoc {
898 pub fn slug(&self) -> std::borrow::Cow<'static, str> {
900 match self {
901 PlatformDoc::Constitution => "constitution".into(),
902 PlatformDoc::GovernanceProtocol => "protocol".into(),
903 PlatformDoc::Prompts => "prompts".into(),
904 PlatformDoc::Prompt(name) => format!("prompt:{name}").into(),
905 }
906 }
907
908 pub fn title(&self) -> std::borrow::Cow<'static, str> {
911 match self {
912 PlatformDoc::Constitution => "The Agora Constitution".into(),
913 PlatformDoc::GovernanceProtocol => {
914 "The Agora Governance Protocol".into()
915 }
916 PlatformDoc::Prompts => "Agora's Model Prompts".into(),
917 PlatformDoc::Prompt(name) => format!("Model prompt: {name}").into(),
918 }
919 }
920
921 pub fn as_prompt(&self) -> Option<&PromptName> {
923 match self {
924 PlatformDoc::Prompt(name) => Some(name),
925 _ => None,
926 }
927 }
928}
929
930impl std::fmt::Display for PlatformDoc {
931 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
932 f.write_str(&self.slug())
933 }
934}
935
936#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
938#[error(
939 "not a platform document (expected \"constitution\", \"protocol\", \
940 \"prompts\" or \"prompt:<name>\"): {0:?}"
941)]
942pub struct PlatformDocError(pub String);
943
944const PROMPT_PREFIXES: [&str; 4] =
948 ["prompt:", "prompt/", "prompts/", "prompts:"];
949
950impl std::str::FromStr for PlatformDoc {
951 type Err = PlatformDocError;
952
953 fn from_str(s: &str) -> Result<Self, Self::Err> {
957 if s.eq_ignore_ascii_case("constitution") {
958 Ok(PlatformDoc::Constitution)
959 } else if s.eq_ignore_ascii_case("protocol")
960 || s.eq_ignore_ascii_case("governance-protocol")
961 {
962 Ok(PlatformDoc::GovernanceProtocol)
963 } else if s.eq_ignore_ascii_case("prompts")
964 || s.eq_ignore_ascii_case("prompt")
965 {
966 Ok(PlatformDoc::Prompts)
967 } else {
968 PROMPT_PREFIXES
969 .iter()
970 .find_map(|prefix| {
971 let head = s.get(..prefix.len())?;
972 head.eq_ignore_ascii_case(prefix)
973 .then(|| s[prefix.len()..].parse().ok())
974 .flatten()
975 })
976 .map(PlatformDoc::Prompt)
977 .ok_or_else(|| PlatformDocError(s.to_string()))
978 }
979 }
980}
981
982impl Serialize for PlatformDoc {
983 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
984 s.collect_str(self)
985 }
986}
987
988impl<'de> Deserialize<'de> for PlatformDoc {
989 fn deserialize<D: serde::Deserializer<'de>>(
990 d: D,
991 ) -> Result<Self, D::Error> {
992 let raw = String::deserialize(d)?;
993 raw.parse().map_err(serde::de::Error::custom)
994 }
995}
996
997#[cfg(feature = "schemars")]
1000impl schemars::JsonSchema for PlatformDoc {
1001 fn inline_schema() -> bool {
1002 true
1003 }
1004
1005 fn schema_name() -> std::borrow::Cow<'static, str> {
1006 std::borrow::Cow::Borrowed("PlatformDoc")
1007 }
1008
1009 fn schema_id() -> std::borrow::Cow<'static, str> {
1010 std::borrow::Cow::Borrowed(concat!(module_path!(), "::PlatformDoc"))
1011 }
1012
1013 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1014 schemars::json_schema!({
1015 "type": "string",
1016 "pattern": PLATFORM_DOC_PATTERN,
1017 "description": "A platform document: \"constitution\", \
1018 \"protocol\" (the Governance Protocol), \
1019 \"prompts\" (the index of published model \
1020 prompts), or \"prompt:<name>\" (one prompt, \
1021 e.g. \"prompt:tier2_reviewer\").",
1022 })
1023 }
1024}
1025
1026#[derive(
1034 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
1035)]
1036#[serde(try_from = "String")]
1037pub struct PromptName(String);
1038
1039#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1041#[error(
1042 "not a prompt name (lowercase letters, digits and `_`, e.g. \
1043 \"tier2_reviewer\"): {0:?}"
1044)]
1045pub struct PromptNameError(pub String);
1046
1047impl PromptName {
1048 pub const MAX_LEN: usize = 64;
1050
1051 pub fn as_str(&self) -> &str {
1053 &self.0
1054 }
1055
1056 fn normalize(s: &str) -> Option<String> {
1057 let s = s.trim();
1058 let s = match s.len().checked_sub(3) {
1059 Some(n)
1060 if s.is_char_boundary(n)
1061 && s[n..].eq_ignore_ascii_case(".md") =>
1062 {
1063 &s[..n]
1064 }
1065 _ => s,
1066 };
1067 let name: String = s
1068 .chars()
1069 .map(|c| match c {
1070 '-' => '_',
1071 c => c.to_ascii_lowercase(),
1072 })
1073 .collect();
1074 let mut chars = name.chars();
1075 let valid = name.len() <= Self::MAX_LEN
1076 && chars.next().is_some_and(|c| c.is_ascii_lowercase())
1077 && chars.all(|c| {
1078 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'
1079 });
1080 valid.then_some(name)
1081 }
1082}
1083
1084impl std::fmt::Display for PromptName {
1085 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086 f.write_str(&self.0)
1087 }
1088}
1089
1090impl AsRef<str> for PromptName {
1091 fn as_ref(&self) -> &str {
1092 &self.0
1093 }
1094}
1095
1096impl std::str::FromStr for PromptName {
1097 type Err = PromptNameError;
1098
1099 fn from_str(s: &str) -> Result<Self, Self::Err> {
1100 Self::normalize(s)
1101 .map(Self)
1102 .ok_or_else(|| PromptNameError(s.to_string()))
1103 }
1104}
1105
1106impl TryFrom<String> for PromptName {
1107 type Error = PromptNameError;
1108
1109 fn try_from(s: String) -> Result<Self, Self::Error> {
1110 s.parse()
1111 }
1112}
1113
1114impl From<PromptName> for PlatformDoc {
1115 fn from(name: PromptName) -> Self {
1116 PlatformDoc::Prompt(name)
1117 }
1118}
1119
1120#[cfg(feature = "schemars")]
1121impl schemars::JsonSchema for PromptName {
1122 fn inline_schema() -> bool {
1123 true
1124 }
1125
1126 fn schema_name() -> std::borrow::Cow<'static, str> {
1127 std::borrow::Cow::Borrowed("PromptName")
1128 }
1129
1130 fn schema_id() -> std::borrow::Cow<'static, str> {
1131 std::borrow::Cow::Borrowed(concat!(module_path!(), "::PromptName"))
1132 }
1133
1134 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1135 schemars::json_schema!({
1136 "type": "string",
1137 "pattern": PROMPT_NAME_PATTERN,
1138 "maxLength": PromptName::MAX_LEN,
1139 "description": "A published model prompt's name, e.g. \
1140 \"tier2_reviewer\". `get_content(\"prompts\")` \
1141 lists them.",
1142 })
1143 }
1144}
1145
1146#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1162pub struct ContentIdPrefix(u32);
1163
1164#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1166#[error(
1167 "not a short id (the first eight hex digits of a UUID, e.g. \"7ad26ccd\"): {0:?}"
1168)]
1169pub struct ContentIdPrefixError(pub String);
1170
1171impl ContentIdPrefix {
1172 pub const LEN: usize = 8;
1174
1175 pub fn of(id: &Uuid) -> Self {
1177 Self((id.as_u128() >> 96) as u32)
1178 }
1179
1180 pub fn bounds(&self) -> (Uuid, Uuid) {
1182 let lo = (self.0 as u128) << 96;
1183 let hi = lo | ((1u128 << 96) - 1);
1184 (Uuid::from_u128(lo), Uuid::from_u128(hi))
1185 }
1186
1187 pub fn matches(&self, id: &Uuid) -> bool {
1189 Self::of(id) == *self
1190 }
1191}
1192
1193impl std::fmt::Display for ContentIdPrefix {
1194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195 write!(f, "{:08x}", self.0)
1196 }
1197}
1198
1199impl std::str::FromStr for ContentIdPrefix {
1200 type Err = ContentIdPrefixError;
1201
1202 fn from_str(s: &str) -> Result<Self, Self::Err> {
1203 let t = s.trim();
1204 if t.len() == Self::LEN && t.bytes().all(|b| b.is_ascii_hexdigit()) {
1205 Ok(Self(u32::from_str_radix(t, 16).expect("eight hex digits")))
1207 } else {
1208 Err(ContentIdPrefixError(s.to_string()))
1209 }
1210 }
1211}
1212
1213impl TryFrom<String> for ContentIdPrefix {
1214 type Error = ContentIdPrefixError;
1215
1216 fn try_from(s: String) -> Result<Self, Self::Error> {
1217 s.parse()
1218 }
1219}
1220
1221impl Serialize for ContentIdPrefix {
1222 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1223 s.collect_str(self)
1224 }
1225}
1226
1227impl<'de> Deserialize<'de> for ContentIdPrefix {
1228 fn deserialize<D: serde::Deserializer<'de>>(
1229 d: D,
1230 ) -> Result<Self, D::Error> {
1231 let raw = String::deserialize(d)?;
1232 raw.parse().map_err(serde::de::Error::custom)
1233 }
1234}
1235
1236impl From<ContentId> for ContentIdPrefix {
1237 fn from(id: ContentId) -> Self {
1238 Self::of(id.as_uuid())
1239 }
1240}
1241
1242#[cfg(feature = "schemars")]
1243impl schemars::JsonSchema for ContentIdPrefix {
1244 fn inline_schema() -> bool {
1245 true
1246 }
1247
1248 fn schema_name() -> std::borrow::Cow<'static, str> {
1249 std::borrow::Cow::Borrowed("ContentIdPrefix")
1250 }
1251
1252 fn schema_id() -> std::borrow::Cow<'static, str> {
1253 std::borrow::Cow::Borrowed(concat!(module_path!(), "::ContentIdPrefix"))
1254 }
1255
1256 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1257 schemars::json_schema!({
1258 "type": "string",
1259 "pattern": CONTENT_ID_PREFIX_PATTERN,
1260 "description": "A short post or comment id: the first eight hex \
1261 digits of its UUID, e.g. \"7ad26ccd\".",
1262 })
1263 }
1264}
1265
1266pub const CONTENT_TARGET_PATTERN: &str = "^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{8})$";
1271
1272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1281pub enum ContentTarget {
1282 Id(ContentId),
1284 Prefix(ContentIdPrefix),
1286}
1287
1288impl ContentTarget {
1289 pub fn full(&self) -> Option<ContentId> {
1291 match self {
1292 ContentTarget::Id(id) => Some(*id),
1293 ContentTarget::Prefix(_) => None,
1294 }
1295 }
1296}
1297
1298impl From<ContentId> for ContentTarget {
1299 fn from(id: ContentId) -> Self {
1300 ContentTarget::Id(id)
1301 }
1302}
1303
1304impl std::fmt::Display for ContentTarget {
1305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1306 match self {
1307 ContentTarget::Id(id) => id.fmt(f),
1308 ContentTarget::Prefix(prefix) => prefix.fmt(f),
1309 }
1310 }
1311}
1312
1313fn ascii_dashes(s: &str) -> String {
1318 s.chars()
1319 .map(|c| match c {
1320 '\u{2010}'..='\u{2014}' | '\u{2212}' => '-',
1321 c => c,
1322 })
1323 .collect()
1324}
1325
1326impl std::str::FromStr for ContentTarget {
1327 type Err = ContentTargetError;
1328
1329 fn from_str(s: &str) -> Result<Self, Self::Err> {
1330 let t = ascii_dashes(s.trim());
1331 if let Ok(id) = t.parse::<Uuid>() {
1332 return Ok(ContentTarget::Id(id.into()));
1333 }
1334 if let Ok(prefix) = t.parse::<ContentIdPrefix>() {
1335 return Ok(ContentTarget::Prefix(prefix));
1336 }
1337 const UUID_LEN: usize = 36;
1340 let leading = t
1341 .get(..UUID_LEN)
1342 .and_then(|head| head.parse::<Uuid>().ok())
1343 .map(ContentId::from);
1344 Err(ContentTargetError {
1345 field: None,
1346 input: s.to_string(),
1347 leading,
1348 })
1349 }
1350}
1351
1352#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1358pub struct ContentTargetError {
1359 field: Option<&'static str>,
1360 input: String,
1361 leading: Option<ContentId>,
1362}
1363
1364impl ContentTargetError {
1365 pub fn in_field(mut self, field: &'static str) -> Self {
1367 self.field = Some(field);
1368 self
1369 }
1370}
1371
1372impl std::fmt::Display for ContentTargetError {
1373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1374 match self.field {
1375 Some(field) => write!(f, "`{field}` must be")?,
1376 None => f.write_str("Expected")?,
1377 }
1378 f.write_str(
1379 " a post or comment id: a full UUID or its first 8 hex digits \
1380 (e.g. \"7ad26ccd\")",
1381 )?;
1382 write!(f, ", not \"{}\"", echo(&self.input))?;
1383 if let Some(id) = self.leading {
1384 write!(
1385 f,
1386 ". It starts with the id {id} followed by extra text; pass only the id"
1387 )?;
1388 }
1389 f.write_str(".")
1390 }
1391}
1392
1393impl Serialize for ContentTarget {
1394 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1395 s.collect_str(self)
1396 }
1397}
1398
1399impl<'de> Deserialize<'de> for ContentTarget {
1400 fn deserialize<D: serde::Deserializer<'de>>(
1401 d: D,
1402 ) -> Result<Self, D::Error> {
1403 content_target::deserialize_field(d, "id")
1404 }
1405}
1406
1407pub mod content_target {
1410 use serde::de::{self, Deserializer, Visitor};
1411
1412 use super::{ContentTarget, ContentTargetError};
1413
1414 struct TargetVisitor(&'static str);
1418
1419 impl Visitor<'_> for TargetVisitor {
1420 type Value = ContentTarget;
1421
1422 fn expecting(
1423 &self,
1424 f: &mut std::fmt::Formatter<'_>,
1425 ) -> std::fmt::Result {
1426 write!(
1427 f,
1428 "`{}` to be a post or comment id: a full UUID or its first 8 hex digits \
1429 (e.g. \"7ad26ccd\")",
1430 self.0
1431 )
1432 }
1433
1434 fn visit_str<E: de::Error>(self, v: &str) -> Result<ContentTarget, E> {
1435 v.parse()
1436 .map_err(|e: ContentTargetError| E::custom(e.in_field(self.0)))
1437 }
1438 }
1439
1440 pub(super) fn deserialize_field<'de, D: Deserializer<'de>>(
1441 d: D,
1442 field: &'static str,
1443 ) -> Result<ContentTarget, D::Error> {
1444 d.deserialize_str(TargetVisitor(field))
1445 }
1446
1447 pub fn reply_to<'de, D: Deserializer<'de>>(
1449 d: D,
1450 ) -> Result<ContentTarget, D::Error> {
1451 deserialize_field(d, "reply_to")
1452 }
1453
1454 pub fn target<'de, D: Deserializer<'de>>(
1456 d: D,
1457 ) -> Result<ContentTarget, D::Error> {
1458 deserialize_field(d, "target")
1459 }
1460
1461 pub fn post_id<'de, D: Deserializer<'de>>(
1463 d: D,
1464 ) -> Result<ContentTarget, D::Error> {
1465 deserialize_field(d, "post_id")
1466 }
1467
1468 pub fn optional_reply_to<'de, D: Deserializer<'de>>(
1470 d: D,
1471 ) -> Result<Option<ContentTarget>, D::Error> {
1472 reply_to(d).map(Some)
1473 }
1474
1475 pub fn optional_target<'de, D: Deserializer<'de>>(
1477 d: D,
1478 ) -> Result<Option<ContentTarget>, D::Error> {
1479 target(d).map(Some)
1480 }
1481}
1482
1483#[cfg(feature = "schemars")]
1487impl schemars::JsonSchema for ContentTarget {
1488 fn inline_schema() -> bool {
1489 true
1490 }
1491
1492 fn schema_name() -> std::borrow::Cow<'static, str> {
1493 std::borrow::Cow::Borrowed("ContentTarget")
1494 }
1495
1496 fn schema_id() -> std::borrow::Cow<'static, str> {
1497 std::borrow::Cow::Borrowed(concat!(module_path!(), "::ContentTarget"))
1498 }
1499
1500 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1501 schemars::json_schema!({
1502 "type": "string",
1503 "pattern": CONTENT_TARGET_PATTERN,
1504 "description": "A post or comment id: its full UUID, or its first \
1505 8 hex digits (e.g. \"7ad26ccd\") as shown by \
1506 get_content and the dashboard. A short id works \
1507 only on unsigned calls; a signed call must use \
1508 the full UUID.",
1509 })
1510 }
1511}
1512
1513#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1516pub struct ContentRefError(pub String);
1517
1518impl ContentRefError {
1519 pub fn leading_ref(&self) -> Option<ContentRef> {
1523 const UUID_LEN: usize = 36;
1524 const CITATION_LEN: usize = "GOV-2026-0006".len();
1525 let s = self.0.trim_start();
1526 let prompt_len = PROMPT_PREFIXES.iter().find_map(|prefix| {
1528 let head = s.get(..prefix.len())?;
1529 head.eq_ignore_ascii_case(prefix).then(|| {
1530 prefix.len()
1531 + s[prefix.len()..]
1532 .find(|c: char| {
1533 !(c.is_ascii_alphanumeric() || c == '_' || c == '-')
1534 })
1535 .unwrap_or(s.len() - prefix.len())
1536 })
1537 });
1538 prompt_len
1540 .into_iter()
1541 .chain([
1542 UUID_LEN,
1543 CITATION_LEN,
1544 "constitution".len(),
1545 "protocol".len(),
1546 "prompts".len(),
1547 ])
1548 .filter(|&n| s.len() > n && s.is_char_boundary(n))
1549 .find_map(|n| {
1553 s[..n]
1554 .parse()
1555 .ok()
1556 .filter(|r| !matches!(r, ContentRef::ContentPrefix(_)))
1557 })
1558 }
1559}
1560
1561impl std::fmt::Display for ContentRefError {
1562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1563 write!(
1564 f,
1565 "not a content reference (expected a post/comment UUID or its \
1566 first eight hex digits, a GOV-YYYY-NNNN / APP-YYYY-NNNN governance id, or a document slug \
1567 like \"constitution\", \"protocol\", \"prompts\" or \
1568 \"prompt:<name>\"): {:?}",
1569 self.0
1570 )?;
1571 if let Some(id) = self.leading_ref() {
1572 write!(
1573 f,
1574 ". It starts with the valid id {id} followed by extra text; \
1575 pass only the id"
1576 )?;
1577 }
1578 Ok(())
1579 }
1580}
1581
1582#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1600pub enum ContentRef {
1601 Content(ContentId),
1603 ContentPrefix(ContentIdPrefix),
1606 Governance(GovernanceLogId),
1608 Document(PlatformDoc),
1610}
1611
1612impl ContentRef {
1613 pub fn as_content(&self) -> Option<ContentId> {
1615 match self {
1616 ContentRef::Content(id) => Some(*id),
1617 _ => None,
1618 }
1619 }
1620
1621 pub fn as_governance(&self) -> Option<&GovernanceLogId> {
1623 match self {
1624 ContentRef::Governance(id) => Some(id),
1625 _ => None,
1626 }
1627 }
1628
1629 pub fn as_document(&self) -> Option<&PlatformDoc> {
1631 match self {
1632 ContentRef::Document(doc) => Some(doc),
1633 _ => None,
1634 }
1635 }
1636
1637 pub fn is_governance(&self) -> bool {
1639 matches!(self, ContentRef::Governance(_))
1640 }
1641
1642 pub fn kind_str(&self) -> &'static str {
1645 match self {
1646 ContentRef::Content(_) | ContentRef::ContentPrefix(_) => "content",
1647 ContentRef::Governance(_) => "governance",
1648 ContentRef::Document(_) => "document",
1649 }
1650 }
1651}
1652
1653impl std::fmt::Display for ContentRef {
1654 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1655 match self {
1656 ContentRef::Content(id) => id.fmt(f),
1657 ContentRef::ContentPrefix(prefix) => prefix.fmt(f),
1658 ContentRef::Governance(id) => id.fmt(f),
1659 ContentRef::Document(doc) => doc.fmt(f),
1660 }
1661 }
1662}
1663
1664impl std::str::FromStr for ContentRef {
1665 type Err = ContentRefError;
1666
1667 fn from_str(s: &str) -> Result<Self, Self::Err> {
1668 if let Ok(id) = s.parse::<ContentId>() {
1669 return Ok(ContentRef::Content(id));
1670 }
1671 if let Ok(prefix) = s.parse::<ContentIdPrefix>() {
1672 return Ok(ContentRef::ContentPrefix(prefix));
1673 }
1674 if let Ok(id) = s.parse::<GovernanceLogId>() {
1675 return Ok(ContentRef::Governance(id));
1676 }
1677 if let Ok(doc) = s.parse::<PlatformDoc>() {
1678 return Ok(ContentRef::Document(doc));
1679 }
1680 Err(ContentRefError(s.to_string()))
1681 }
1682}
1683
1684impl TryFrom<String> for ContentRef {
1685 type Error = ContentRefError;
1686
1687 fn try_from(s: String) -> Result<Self, Self::Error> {
1688 s.parse()
1689 }
1690}
1691
1692impl From<ContentId> for ContentRef {
1693 fn from(id: ContentId) -> Self {
1694 ContentRef::Content(id)
1695 }
1696}
1697
1698impl From<PostId> for ContentRef {
1699 fn from(id: PostId) -> Self {
1700 ContentRef::Content(id.into())
1701 }
1702}
1703
1704impl From<CommentId> for ContentRef {
1705 fn from(id: CommentId) -> Self {
1706 ContentRef::Content(id.into())
1707 }
1708}
1709
1710impl From<ContentIdPrefix> for ContentRef {
1711 fn from(prefix: ContentIdPrefix) -> Self {
1712 ContentRef::ContentPrefix(prefix)
1713 }
1714}
1715
1716impl From<GovernanceLogId> for ContentRef {
1717 fn from(id: GovernanceLogId) -> Self {
1718 ContentRef::Governance(id)
1719 }
1720}
1721
1722impl From<PlatformDoc> for ContentRef {
1723 fn from(doc: PlatformDoc) -> Self {
1724 ContentRef::Document(doc)
1725 }
1726}
1727
1728impl Serialize for ContentRef {
1729 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1730 s.collect_str(self)
1731 }
1732}
1733
1734impl<'de> Deserialize<'de> for ContentRef {
1735 fn deserialize<D: serde::Deserializer<'de>>(
1736 d: D,
1737 ) -> Result<Self, D::Error> {
1738 let raw = String::deserialize(d)?;
1739 raw.parse().map_err(serde::de::Error::custom)
1740 }
1741}
1742
1743#[cfg(feature = "schemars")]
1748impl schemars::JsonSchema for ContentRef {
1749 fn inline_schema() -> bool {
1750 true
1751 }
1752
1753 fn schema_name() -> std::borrow::Cow<'static, str> {
1754 std::borrow::Cow::Borrowed("ContentRef")
1755 }
1756
1757 fn schema_id() -> std::borrow::Cow<'static, str> {
1758 std::borrow::Cow::Borrowed(concat!(module_path!(), "::ContentRef"))
1759 }
1760
1761 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1762 schemars::json_schema!({
1763 "type": "string",
1764 "pattern": CONTENT_REF_PATTERN,
1765 "description": "A post or comment UUID, or its first eight \
1766 hex digits (e.g. \"7ad26ccd\"); a governance log id \
1767 such as \"GOV-2026-0006\" (Council decision) \
1768 or \"APP-2026-0003\" (appeals ruling); or a \
1769 document slug — \"constitution\", \
1770 \"protocol\" (the Governance Protocol), \
1771 \"prompts\" (the index of the model prompts \
1772 moderation, appeals and the Council run on) or \
1773 \"prompt:<name>\" (one of them).",
1774 })
1775 }
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780 use super::*;
1781
1782 #[test]
1783 fn oauth_client_id_round_trips() {
1784 let id = OAuthClientId::generate();
1785 assert_eq!(id.as_str().parse::<OAuthClientId>().unwrap(), id);
1786 let json = serde_json::to_string(&id).unwrap();
1787 assert_eq!(serde_json::from_str::<OAuthClientId>(&json).unwrap(), id);
1788 }
1789
1790 #[test]
1791 fn oauth_client_id_rejects_empty_oversized_and_control_characters() {
1792 assert!("".parse::<OAuthClientId>().is_err());
1793 assert!("a".repeat(256).parse::<OAuthClientId>().is_err());
1794 assert!("abc\ndef".parse::<OAuthClientId>().is_err());
1795 assert!(serde_json::from_str::<OAuthClientId>("\"\"").is_err());
1796 }
1797
1798 #[test]
1799 fn content_ref_error_names_a_leading_id_followed_by_extra_text() {
1800 let uuid = "6dcef9bb-2b3c-4f5e-9a1b-0c2d3e4f5a6b";
1801 for (input, lead) in [
1802 (format!("{uuid} and the safe-space proposal"), uuid),
1803 (format!("{uuid}b0518e42"), uuid),
1804 (
1805 "GOV-2026-0006 (the ratification)".to_string(),
1806 "GOV-2026-0006",
1807 ),
1808 ("protocol, section 3".to_string(), "protocol"),
1809 ("prompts, please".to_string(), "prompts"),
1810 (
1811 "prompt:tier2_reviewer and the juror's".to_string(),
1812 "prompt:tier2_reviewer",
1813 ),
1814 (
1815 "prompts/appeals_juror.md, line 3".to_string(),
1816 "prompt:appeals_juror",
1817 ),
1818 ] {
1819 let err = input.parse::<ContentRef>().unwrap_err();
1820 assert_eq!(
1821 err.leading_ref(),
1822 Some(lead.parse().unwrap()),
1823 "{input}"
1824 );
1825 let msg = err.to_string();
1826 assert!(
1827 msg.contains(&format!("valid id {lead} followed")),
1828 "{msg}"
1829 );
1830 }
1831
1832 let short = "6dcef9bb-2b3c-4f5e-9a1b-0c2d3e4f5a6";
1834 let err = short.parse::<ContentRef>().unwrap_err();
1835 assert_eq!(err.leading_ref(), None);
1836 assert!(!err.to_string().contains("followed by"));
1837 assert_eq!(
1839 "GOV-2026-1".parse::<ContentRef>().unwrap(),
1840 ContentRef::Governance("GOV-2026-0001".parse().unwrap())
1841 );
1842 }
1843
1844 #[test]
1845 fn citations_normalize_as_written() {
1846 for (written, canonical) in [
1847 ("GOV-2026-0006", "GOV-2026-0006"),
1848 ("GOV-2026.6", "GOV-2026-0006"),
1849 ("gov-2026-6", "GOV-2026-0006"),
1850 ("GOV 2026 6", "GOV-2026-0006"),
1851 ("app/2026/03", "APP-2026-0003"),
1852 (" REC-2026-0002 ", "REC-2026-0002"),
1853 ] {
1854 let id: GovernanceLogId = written.parse().unwrap();
1855 assert_eq!(id.as_str(), canonical, "{written}");
1856 let json: GovernanceLogId =
1857 serde_json::from_value(serde_json::json!(written)).unwrap();
1858 assert_eq!(json.as_str(), canonical, "{written} via serde");
1859 let content: ContentRef = written.parse().unwrap();
1860 assert_eq!(content, ContentRef::Governance(id), "{written}");
1861 }
1862 for rejected in [
1863 "GOV-2026-N",
1864 "GOV-2026/GOVG=6",
1865 "GOV-2026-00006",
1866 "GOV-26-0006",
1867 "GOV-2026-",
1868 "XYZ-2026-0006",
1869 "GOV-2026-0006-1",
1870 ] {
1871 assert!(rejected.parse::<GovernanceLogId>().is_err(), "{rejected}");
1872 }
1873 assert!(!GovernanceLogId::is_citation_shaped("GOV-2026.6"));
1875 }
1876
1877 #[test]
1878 fn content_ref_parses_document_slugs() {
1879 assert_eq!(
1880 "constitution".parse(),
1881 Ok(ContentRef::Document(PlatformDoc::Constitution))
1882 );
1883 assert_eq!(
1884 "protocol".parse(),
1885 Ok(ContentRef::Document(PlatformDoc::GovernanceProtocol))
1886 );
1887 assert_eq!(
1889 "governance-protocol".parse(),
1890 Ok(ContentRef::Document(PlatformDoc::GovernanceProtocol))
1891 );
1892 assert_eq!(
1893 "Constitution".parse(),
1894 Ok(ContentRef::Document(PlatformDoc::Constitution))
1895 );
1896 assert!("proto".parse::<ContentRef>().is_err());
1897 }
1898
1899 #[test]
1900 fn content_ref_parses_prompt_slugs() {
1901 let juror = PlatformDoc::Prompt("appeals_juror".parse().unwrap());
1902 for written in [
1903 "prompt:appeals_juror",
1904 "Prompt:Appeals_Juror",
1905 "prompt/appeals-juror",
1906 "prompts/appeals_juror.md",
1907 "prompts:appeals_juror",
1908 ] {
1909 assert_eq!(
1910 written.parse(),
1911 Ok(ContentRef::Document(juror.clone())),
1912 "{written}"
1913 );
1914 }
1915 assert_eq!(juror.to_string(), "prompt:appeals_juror");
1916 for index in ["prompts", "Prompts", "prompt"] {
1917 assert_eq!(
1918 index.parse(),
1919 Ok(ContentRef::Document(PlatformDoc::Prompts)),
1920 "{index}"
1921 );
1922 }
1923 for rejected in [
1924 "prompt:",
1925 "prompt:2fast",
1926 "prompt:tier 2",
1927 "prompt:../secrets",
1928 "prompt:tier2_reviewer/x",
1929 "promptly",
1930 ] {
1931 assert!(rejected.parse::<ContentRef>().is_err(), "{rejected}");
1932 }
1933 let long = format!("prompt:{}", "a".repeat(PromptName::MAX_LEN + 1));
1934 assert!(long.parse::<ContentRef>().is_err());
1935 }
1936
1937 #[test]
1938 fn content_id_prefix_parses_eight_hex_digits_leniently() {
1939 let p: ContentIdPrefix = "7ad26ccd".parse().unwrap();
1940 assert_eq!(p.to_string(), "7ad26ccd");
1941 assert_eq!(" 7AD26CCD\n".parse::<ContentIdPrefix>().unwrap(), p);
1942 for rejected in ["7ad26cc", "7ad26ccd0", "7ad26ccg", "", "7ad2-6cc"] {
1943 assert!(rejected.parse::<ContentIdPrefix>().is_err(), "{rejected}");
1944 }
1945 }
1946
1947 #[test]
1948 fn content_id_prefix_bounds_cover_exactly_its_uuids() {
1949 let id: Uuid = "7ad26ccd-922f-484a-a37c-51777344a98c".parse().unwrap();
1950 let p = ContentIdPrefix::of(&id);
1951 assert_eq!(p.to_string(), "7ad26ccd");
1952 assert!(p.matches(&id));
1953 let (lo, hi) = p.bounds();
1954 assert_eq!(lo.to_string(), "7ad26ccd-0000-0000-0000-000000000000");
1955 assert_eq!(hi.to_string(), "7ad26ccd-ffff-ffff-ffff-ffffffffffff");
1956 assert!(lo <= id && id <= hi);
1957 let next: Uuid =
1958 "7ad26cce-0000-0000-0000-000000000000".parse().unwrap();
1959 assert!(!p.matches(&next) && next > hi);
1960 let top: ContentIdPrefix = "ffffffff".parse().unwrap();
1962 assert_eq!(top.bounds().1, Uuid::max());
1963 let bottom: ContentIdPrefix = "00000000".parse().unwrap();
1964 assert_eq!(bottom.bounds().0, Uuid::nil());
1965 }
1966
1967 #[test]
1968 fn content_ref_reads_a_short_id_and_only_eight_digits() {
1969 let r: ContentRef = "7ad26ccd".parse().unwrap();
1970 assert_eq!(r, ContentRef::ContentPrefix("7ad26ccd".parse().unwrap()));
1971 assert_eq!(r.kind_str(), "content");
1972 assert_eq!(serde_json::to_string(&r).unwrap(), "\"7ad26ccd\"");
1973 let full: ContentRef =
1975 "7ad26ccd-922f-484a-a37c-51777344a98c".parse().unwrap();
1976 assert!(matches!(full, ContentRef::Content(_)));
1977 let err = "7ad26ccd-922f".parse::<ContentRef>().unwrap_err();
1982 assert_eq!(err.leading_ref(), None);
1983 }
1984
1985 #[test]
1986 fn content_ref_pattern_admits_short_ids() {
1987 let short = CONTENT_ID_PREFIX_PATTERN
1988 .strip_prefix('^')
1989 .and_then(|p| p.strip_suffix('$'))
1990 .unwrap();
1991 assert!(CONTENT_REF_PATTERN.contains(&format!("|{short}|")));
1992 }
1993
1994 #[test]
1996 fn document_patterns_agree() {
1997 let docs = PLATFORM_DOC_PATTERN.strip_prefix("^(").unwrap();
1998 assert!(CONTENT_REF_PATTERN.ends_with(&format!("|{docs}")));
1999 let name = PROMPT_NAME_PATTERN
2000 .strip_prefix('^')
2001 .and_then(|p| p.strip_suffix('$'))
2002 .unwrap();
2003 assert!(PLATFORM_DOC_PATTERN.contains(&format!("|prompt:{name})$")));
2004 }
2005
2006 #[test]
2007 fn platform_doc_serde_uses_the_canonical_slug() {
2008 let json =
2009 serde_json::to_string(&PlatformDoc::GovernanceProtocol).unwrap();
2010 assert_eq!(json, "\"protocol\"");
2011 let doc: PlatformDoc =
2012 serde_json::from_str("\"governance-protocol\"").unwrap();
2013 assert_eq!(doc, PlatformDoc::GovernanceProtocol);
2014 }
2015
2016 #[test]
2017 fn ids_are_unique() {
2018 let a = AgentId::new();
2019 let b = AgentId::new();
2020 assert_ne!(a, b);
2021 }
2022
2023 #[test]
2024 fn serde_round_trip() {
2025 let id = PostId::new();
2026 let json = serde_json::to_string(&id).unwrap();
2027 let deserialized: PostId = serde_json::from_str(&json).unwrap();
2028 assert_eq!(id, deserialized);
2029 }
2030
2031 #[test]
2032 fn display_shows_uuid() {
2033 let id = CommunityId::new();
2034 let display = id.to_string();
2035 assert_eq!(display.len(), 36);
2037 assert!(display.contains('-'));
2038 }
2039
2040 #[test]
2041 fn from_uuid_round_trip() {
2042 let uuid = Uuid::new_v4();
2043 let id = AgentId::from(uuid);
2044 let back: Uuid = id.into();
2045 assert_eq!(uuid, back);
2046 }
2047
2048 #[test]
2052 fn every_id_round_trips_through_its_own_display() {
2053 let agent = AgentId::new();
2054 assert_eq!(agent.to_string().parse::<AgentId>().unwrap(), agent);
2055
2056 let action = ModerationActionId::new();
2057 assert_eq!(
2058 action.to_string().parse::<ModerationActionId>().unwrap(),
2059 action
2060 );
2061
2062 let content = ContentId::new();
2063 assert_eq!(content.to_string().parse::<ContentId>().unwrap(), content);
2064 }
2065
2066 #[test]
2067 fn parsing_a_non_uuid_is_an_error_not_a_panic() {
2068 assert!("not-a-uuid".parse::<ContentId>().is_err());
2069 assert!("".parse::<ContentId>().is_err());
2070 }
2071
2072 #[test]
2077 fn content_id_is_wire_compatible_with_a_bare_uuid() {
2078 let uuid = Uuid::new_v4();
2079 let typed = ContentId::from(uuid);
2080 assert_eq!(
2081 serde_json::to_string(&typed).unwrap(),
2082 serde_json::to_string(&uuid).unwrap()
2083 );
2084 }
2085
2086 #[test]
2089 fn every_moderation_target_narrows_losslessly() {
2090 let uuid = Uuid::new_v4();
2091
2092 for (label, got) in [
2093 ("PostId", ModerationTargetId::from(PostId::from(uuid))),
2094 ("CommentId", ModerationTargetId::from(CommentId::from(uuid))),
2095 ("MessageId", ModerationTargetId::from(MessageId::from(uuid))),
2096 ("AgentId", ModerationTargetId::from(AgentId::from(uuid))),
2097 ("ContentId", ModerationTargetId::from(ContentId::from(uuid))),
2098 ] {
2099 assert_eq!(
2100 got.as_uuid(),
2101 &uuid,
2102 "{label} -> ModerationTargetId lost the uuid"
2103 );
2104 }
2105 }
2106
2107 #[test]
2111 fn resolved_ids_narrow_to_content_id_losslessly() {
2112 let uuid = Uuid::new_v4();
2113
2114 assert_eq!(
2115 ContentId::from(PostId::from(uuid)).as_uuid(),
2116 &uuid,
2117 "PostId -> ContentId lost the uuid"
2118 );
2119 assert_eq!(
2120 ContentId::from(CommentId::from(uuid)).as_uuid(),
2121 &uuid,
2122 "CommentId -> ContentId lost the uuid"
2123 );
2124 assert_eq!(
2125 ContentId::from(PostOrCommentId::Comment(CommentId::from(uuid)))
2126 .as_uuid(),
2127 &uuid,
2128 "PostOrCommentId -> ContentId lost the uuid"
2129 );
2130 }
2131
2132 #[test]
2133 fn json_is_plain_uuid_string() {
2134 let uuid = Uuid::new_v4();
2135 let id = AgentId::from(uuid);
2136 let id_json = serde_json::to_string(&id).unwrap();
2138 let uuid_json = serde_json::to_string(&uuid).unwrap();
2139 assert_eq!(id_json, uuid_json);
2140 }
2141
2142 #[cfg(feature = "schemars")]
2147 #[test]
2148 fn id_json_schema_is_inlined() {
2149 use schemars::JsonSchema;
2150
2151 assert!(
2152 <PostId as JsonSchema>::inline_schema(),
2153 "PostId::inline_schema() must return true to avoid $ref in containing schemas"
2154 );
2155 assert!(<AgentId as JsonSchema>::inline_schema());
2156 assert!(<CommentId as JsonSchema>::inline_schema());
2157 assert!(<CommunityId as JsonSchema>::inline_schema());
2158 assert!(<GovernanceLogId as JsonSchema>::inline_schema());
2159 assert!(<ContentRef as JsonSchema>::inline_schema());
2160
2161 #[derive(schemars::JsonSchema)]
2165 #[allow(dead_code)]
2166 struct Container {
2167 post_id: PostId,
2169 agent_id: Option<AgentId>,
2171 gov_id: GovernanceLogId,
2173 maybe_gov_id: Option<GovernanceLogId>,
2175 content_ref: ContentRef,
2177 maybe_content_ref: Option<ContentRef>,
2179 }
2180
2181 let schema = schemars::schema_for!(Container);
2182 let value = serde_json::to_value(&schema).unwrap();
2183
2184 assert!(
2186 value.get("$defs").is_none(),
2187 "no $defs should be emitted for ID-only container; got schema: {value}"
2188 );
2189
2190 let post_id = &value["properties"]["post_id"];
2192 assert!(
2193 post_id.get("$ref").is_none(),
2194 "post_id must not be a $ref; got: {post_id}"
2195 );
2196 assert_eq!(post_id["type"], "string");
2197 assert_eq!(post_id["format"], "uuid");
2198
2199 let agent_id = &value["properties"]["agent_id"];
2204 assert!(
2205 agent_id.get("$ref").is_none(),
2206 "agent_id must not be a $ref; got: {agent_id}"
2207 );
2208 let agent_id_str = agent_id.to_string();
2209 assert!(
2210 !agent_id_str.contains("$ref"),
2211 "agent_id schema must contain no $ref anywhere; got: {agent_id}"
2212 );
2213 assert!(
2214 agent_id_str.contains("\"format\":\"uuid\""),
2215 "agent_id should still carry format=uuid; got: {agent_id}"
2216 );
2217
2218 for field in
2223 ["gov_id", "maybe_gov_id", "content_ref", "maybe_content_ref"]
2224 {
2225 let f = &value["properties"][field];
2226 assert!(
2227 !f.to_string().contains("$ref"),
2228 "{field} must contain no $ref anywhere; got: {f}"
2229 );
2230 }
2231 assert_eq!(value["properties"]["gov_id"]["type"], "string");
2232 assert_eq!(
2233 value["properties"]["gov_id"]["pattern"],
2234 GOVERNANCE_LOG_ID_PATTERN
2235 );
2236 assert!(
2237 value["properties"]["maybe_gov_id"]
2238 .to_string()
2239 .contains("GOV|APP"),
2240 "Option<GovernanceLogId> should keep the citation pattern; got: {}",
2241 value["properties"]["maybe_gov_id"]
2242 );
2243 assert_eq!(value["properties"]["content_ref"]["type"], "string");
2244 }
2245
2246 #[test]
2247 fn governance_log_id_accepts_only_citation_shapes() {
2248 for (good, prefix) in [
2249 ("GOV-2026-0006", GovernanceLogPrefix::Gov),
2250 ("APP-2026-0003", GovernanceLogPrefix::App),
2251 ("AMD-2026-0001", GovernanceLogPrefix::Amd),
2252 ("KEY-2026-0001", GovernanceLogPrefix::Key),
2253 ("REC-2026-0001", GovernanceLogPrefix::Rec),
2254 ("GOV-1999-0000", GovernanceLogPrefix::Gov),
2255 ] {
2256 let id = good.parse::<GovernanceLogId>().unwrap();
2257 assert_eq!(id.as_str(), good, "{good} should parse");
2258 assert_eq!(id.prefix(), prefix);
2259 assert_eq!(id.prefix().as_str(), &good[..3]);
2260 }
2261 assert_eq!(
2262 GovernanceLogPrefix::ALL
2263 .map(|p| p.to_string())
2264 .concat()
2265 .len(),
2266 3 * GovernanceLogPrefix::ALL.len()
2267 );
2268 assert_eq!(
2270 "GOV-2026-006".parse::<GovernanceLogId>().unwrap().as_str(),
2271 "GOV-2026-0006"
2272 );
2273 assert_eq!(
2274 "gov-2026-0006".parse::<GovernanceLogId>().unwrap().as_str(),
2275 "GOV-2026-0006"
2276 );
2277 for bad in [
2278 "",
2279 "GOV-26-0006",
2280 "MOD-2026-0006",
2281 "GOV-2026-0006-1",
2282 "GOV-202X-0006",
2283 "3f1a0000-0000-0000-0000-000000000000",
2284 ] {
2285 assert!(
2286 bad.parse::<GovernanceLogId>().is_err(),
2287 "{bad:?} should not parse as a GovernanceLogId"
2288 );
2289 }
2290 }
2291
2292 #[test]
2296 fn governance_log_id_is_wire_compatible_with_a_bare_string() {
2297 let id: GovernanceLogId = "GOV-2026-0006".parse().unwrap();
2298 assert_eq!(serde_json::to_string(&id).unwrap(), "\"GOV-2026-0006\"");
2299 let back: GovernanceLogId =
2300 serde_json::from_str("\"GOV-2026-0006\"").unwrap();
2301 assert_eq!(back, id);
2302 assert!(serde_json::from_str::<GovernanceLogId>("\"nope\"").is_err());
2304 }
2305
2306 #[test]
2309 fn content_ref_round_trips_as_a_bare_string() {
2310 let uuid = Uuid::new_v4();
2311 let content = ContentRef::from(ContentId::from(uuid));
2312 assert_eq!(
2313 serde_json::to_value(&content).unwrap(),
2314 serde_json::json!(uuid.to_string())
2315 );
2316 assert_eq!(
2317 serde_json::from_value::<ContentRef>(serde_json::json!(
2318 uuid.to_string()
2319 ))
2320 .unwrap(),
2321 content
2322 );
2323
2324 let gov = ContentRef::Governance("APP-2026-0003".parse().unwrap());
2325 assert_eq!(
2326 serde_json::to_value(&gov).unwrap(),
2327 serde_json::json!("APP-2026-0003")
2328 );
2329 assert_eq!(
2330 serde_json::from_value::<ContentRef>(serde_json::json!(
2331 "APP-2026-0003"
2332 ))
2333 .unwrap(),
2334 gov
2335 );
2336
2337 assert!(gov.is_governance());
2338 assert!(!content.is_governance());
2339 assert_eq!(gov.kind_str(), "governance");
2340 assert_eq!(content.kind_str(), "content");
2341 assert_eq!(content.as_content(), Some(ContentId::from(uuid)));
2342 assert!(content.as_governance().is_none());
2343
2344 assert!("not-an-id".parse::<ContentRef>().is_err());
2346 assert!(
2347 serde_json::from_value::<ContentRef>(serde_json::json!(
2348 "not-an-id"
2349 ))
2350 .is_err()
2351 );
2352 }
2353
2354 #[test]
2356 fn every_readable_id_narrows_to_a_content_ref() {
2357 let uuid = Uuid::new_v4();
2358 for (label, got) in [
2359 ("PostId", ContentRef::from(PostId::from(uuid))),
2360 ("CommentId", ContentRef::from(CommentId::from(uuid))),
2361 ("ContentId", ContentRef::from(ContentId::from(uuid))),
2362 ] {
2363 assert_eq!(
2364 got,
2365 ContentRef::Content(ContentId::from(uuid)),
2366 "{label} -> ContentRef lost the uuid"
2367 );
2368 }
2369 let gov: GovernanceLogId = "GOV-2026-0006".parse().unwrap();
2370 assert_eq!(ContentRef::from(gov.clone()), ContentRef::Governance(gov));
2371 }
2372
2373 #[test]
2376 fn string_shaped_ids_round_trip_through_display() {
2377 let gov: GovernanceLogId = "GOV-2026-0006".parse().unwrap();
2378 assert_eq!(gov.to_string().parse::<GovernanceLogId>().unwrap(), gov);
2379
2380 let r = ContentRef::Governance(gov);
2381 assert_eq!(r.to_string().parse::<ContentRef>().unwrap(), r);
2382
2383 let r = ContentRef::Content(ContentId::new());
2384 assert_eq!(r.to_string().parse::<ContentRef>().unwrap(), r);
2385 }
2386
2387 #[test]
2388 fn post_or_comment_post_variant() {
2389 let inner = PostId::new();
2390 let tagged = PostOrCommentId::Post(inner);
2391 assert!(tagged.is_post());
2392 assert!(!tagged.is_comment());
2393 assert_eq!(tagged.as_post(), Some(inner));
2394 assert_eq!(tagged.as_comment(), None);
2395 assert_eq!(tagged.as_uuid(), *inner.as_uuid());
2396 assert_eq!(tagged.kind_str(), "post");
2397 }
2398
2399 #[test]
2400 fn post_or_comment_comment_variant() {
2401 let inner = CommentId::new();
2402 let tagged = PostOrCommentId::Comment(inner);
2403 assert!(tagged.is_comment());
2404 assert!(!tagged.is_post());
2405 assert_eq!(tagged.as_comment(), Some(inner));
2406 assert_eq!(tagged.as_post(), None);
2407 assert_eq!(tagged.as_uuid(), *inner.as_uuid());
2408 assert_eq!(tagged.kind_str(), "comment");
2409 }
2410
2411 #[test]
2412 fn post_or_comment_from_conversions() {
2413 let post = PostId::new();
2414 let comment = CommentId::new();
2415 let via_post: PostOrCommentId = post.into();
2416 let via_comment: PostOrCommentId = comment.into();
2417 assert_eq!(via_post, PostOrCommentId::Post(post));
2418 assert_eq!(via_comment, PostOrCommentId::Comment(comment));
2419 }
2420
2421 #[test]
2422 fn post_or_comment_display_is_kind_colon_uuid() {
2423 let post = PostId::new();
2424 let tagged = PostOrCommentId::Post(post);
2425 let rendered = tagged.to_string();
2426 assert!(rendered.starts_with("post:"));
2427 assert!(rendered.contains(&post.to_string()));
2428 }
2429
2430 const FULL: &str = "7ad26ccd-922f-484a-a37c-51777344a98c";
2433
2434 #[test]
2437 fn a_bad_id_fails_in_plain_words() {
2438 let bad = "7ad26ccd-922f-484a-a37c-51777344a";
2439 let want = format!(
2440 "not a valid PostId: expected a UUID like \"{FULL}\", got \"{bad}\""
2441 );
2442 assert_eq!(bad.parse::<PostId>().unwrap_err().to_string(), want);
2443 let err = serde_json::from_str::<PostId>(&format!("\"{bad}\""))
2444 .unwrap_err()
2445 .to_string();
2446 assert!(err.starts_with(&want), "{err}");
2447 assert!(!err.contains("group"), "{err}");
2448 let err = serde_json::from_str::<AgentId>("5")
2449 .unwrap_err()
2450 .to_string();
2451 assert!(err.contains("expected AgentId as a UUID string"), "{err}");
2452 let long = "x".repeat(500);
2454 let msg = long.parse::<CommentId>().unwrap_err().to_string();
2455 assert!(msg.len() < 200 && msg.ends_with("…\""), "{msg}");
2456 }
2457
2458 #[test]
2460 fn ids_still_round_trip() {
2461 let id: PostId = FULL.parse().unwrap();
2462 let json = serde_json::to_string(&id).unwrap();
2463 assert_eq!(serde_json::from_str::<PostId>(&json).unwrap(), id);
2464 let map: std::collections::HashMap<PostId, i64> =
2465 [(id, 3)].into_iter().collect();
2466 let json = serde_json::to_string(&map).unwrap();
2467 let back: std::collections::HashMap<PostId, i64> =
2468 serde_json::from_str(&json).unwrap();
2469 assert_eq!(back, map);
2470 }
2471
2472 fn target(s: &str) -> Result<ContentTarget, ContentTargetError> {
2473 s.parse()
2474 }
2475
2476 #[test]
2477 fn full_and_short_targets_parse() {
2478 let full = target(FULL).unwrap();
2479 assert_eq!(full.full().unwrap().to_string(), FULL);
2480 assert_eq!(full.to_string(), FULL);
2481 let short = target(" 7AD26CCD ").unwrap();
2482 assert!(short.full().is_none());
2483 assert_eq!(short.to_string(), "7ad26ccd");
2484 for variant in [
2486 FULL.replace('-', ""),
2487 FULL.to_uppercase(),
2488 FULL.replace('-', "\u{2011}"),
2489 ] {
2490 assert_eq!(target(&variant).unwrap(), full, "{variant}");
2491 }
2492 }
2493
2494 #[test]
2495 fn a_truncated_uuid_is_an_error_not_its_prefix() {
2496 let err = target("7ad26ccd-922f-484a-a37c-51777344a").unwrap_err();
2497 let msg = err.in_field("reply_to").to_string();
2498 assert!(
2499 msg.starts_with(
2500 "`reply_to` must be a post or comment id: a full UUID or its first 8 hex digits"
2501 ),
2502 "{msg}"
2503 );
2504 for internal in [
2505 "group",
2506 "UUID parsing",
2507 "invalid length",
2508 "invalid character",
2509 ] {
2510 assert!(!msg.contains(internal), "{internal} leaked: {msg}");
2511 }
2512 assert!(target("7ad26cc").is_err());
2513 assert!(target("7ad26ccd9").is_err());
2514 }
2515
2516 #[test]
2517 fn trailing_text_names_the_id_it_starts_with() {
2518 let msg = target(&format!("{FULL} (the reply above)"))
2519 .unwrap_err()
2520 .to_string();
2521 assert!(msg.contains(&format!("starts with the id {FULL}")), "{msg}");
2522 }
2523
2524 #[derive(Debug, serde::Deserialize)]
2525 struct Probe {
2526 #[serde(deserialize_with = "content_target::reply_to")]
2527 #[allow(dead_code)]
2528 reply_to: ContentTarget,
2529 }
2530
2531 #[test]
2532 fn target_errors_name_the_field_for_strings_and_other_types() {
2533 let err = serde_json::from_str::<Probe>(r#"{"reply_to": "nope"}"#)
2534 .unwrap_err();
2535 assert!(
2536 err.to_string()
2537 .starts_with("`reply_to` must be a post or comment id"),
2538 "{err}"
2539 );
2540 let err =
2541 serde_json::from_str::<Probe>(r#"{"reply_to": 5}"#).unwrap_err();
2542 assert!(
2543 err.to_string()
2544 .contains("expected `reply_to` to be a post or comment id"),
2545 "{err}"
2546 );
2547 }
2548
2549 #[cfg(feature = "schemars")]
2550 #[test]
2551 fn target_schema_is_inline_and_admits_both_forms() {
2552 let schema = schemars::schema_for!(ContentTarget);
2553 let v = schema.as_value();
2554 assert!(v.get("$ref").is_none() && v.get("$defs").is_none(), "{v}");
2555 assert!(v.get("format").is_none(), "a short id is not format: uuid");
2556 assert_eq!(v["pattern"], CONTENT_TARGET_PATTERN);
2557 }
2558}