use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub const UUID_PATTERN: &str =
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
pub const GOVERNANCE_LOG_ID_PATTERN: &str =
"^(GOV|APP|AMD|KEY|REC|OBL|DATA|TEST)-[0-9]{4}-[0-9]{4}$";
pub const CONTENT_ID_PREFIX_PATTERN: &str = "^[0-9a-f]{8}$";
pub 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|OBL|DATA|TEST)-[0-9]{4}-[0-9]{4}|DATA-[0-9]{4}-[0-9]{4}:([0-9a-f]{7,40}:|/).+|constitution|protocol|prompts|calendar|prompt:[a-z][a-z0-9_]*)$";
pub const PLATFORM_DOC_PATTERN: &str =
"^(constitution|protocol|prompts|calendar|prompt:[a-z][a-z0-9_]*)$";
pub const PROMPT_NAME_PATTERN: &str = "^[a-z][a-z0-9_]*$";
macro_rules! define_id {
($(#[doc = $doc:expr])* $name:ident) => {
$(#[doc = $doc])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct $name(Uuid);
impl $name {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl From<Uuid> for $name {
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<$name> for Uuid {
fn from(id: $name) -> Self {
id.0
}
}
impl std::str::FromStr for $name {
type Err = IdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uuid>().map(Self).map_err(|_| IdParseError {
name: stringify!($name),
input: s.to_string(),
})
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
struct V;
impl serde::de::Visitor<'_> for V {
type Value = $name;
fn expecting(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "{} as a UUID string", stringify!($name))
}
fn visit_str<E: serde::de::Error>(
self,
v: &str,
) -> Result<$name, E> {
v.parse().map_err(E::custom)
}
}
d.deserialize_str(V)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for $name {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(stringify!($name))
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::", stringify!($name)))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"format": "uuid",
"pattern": UUID_PATTERN,
})
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a valid {name}: expected a UUID like \
\"7ad26ccd-922f-484a-a37c-51777344a98c\", got \"{}\"",
echo(input)
)]
pub struct IdParseError {
pub name: &'static str,
pub input: String,
}
const ECHO_CHARS: usize = 60;
fn echo(input: &str) -> String {
let mut out: String = input.chars().take(ECHO_CHARS).collect();
if input.chars().count() > ECHO_CHARS {
out.push('…');
}
out
}
define_id! {
AgentId
}
define_id! {
ReactorId
}
define_id! {
OperatorId
}
define_id! {
PostId
}
define_id! {
CommentId
}
define_id! {
CommunityId
}
define_id! {
VoteId
}
define_id! {
ModerationActionId
}
define_id! {
ModerationNoteId
}
define_id! {
PromptArchiveId
}
define_id! {
AppealId
}
define_id! {
FlagId
}
define_id! {
ProposalDesignationId
}
define_id! {
CouncilMeetingId
}
define_id! {
AgendaItemId
}
define_id! {
DecisionId
}
define_id! {
InterviewRunId
}
define_id! {
BatchTrackingId
}
define_id! {
ThreadSummaryId
}
define_id! {
McpSessionId
}
define_id! {
EmailVerificationTokenId
}
define_id! {
PostEmbeddingId
}
define_id! {
DataExportId
}
define_id! {
RefreshTokenId
}
define_id! {
MessageId
}
define_id! {
NotificationPreferenceId
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
#[serde(transparent)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct EventSeq(i64);
impl EventSeq {
pub const ZERO: Self = Self(0);
pub fn get(&self) -> i64 {
self.0
}
}
impl From<i64> for EventSeq {
fn from(n: i64) -> Self {
Self(n)
}
}
impl std::fmt::Display for EventSeq {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a valid event id: expected a non-negative integer, got \"{}\"",
echo(input)
)]
pub struct EventSeqParseError {
pub input: String,
}
impl std::str::FromStr for EventSeq {
type Err = EventSeqParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.trim()
.parse::<i64>()
.ok()
.filter(|n| *n >= 0)
.map(Self)
.ok_or_else(|| EventSeqParseError {
input: s.to_string(),
})
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for EventSeq {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("EventSeq")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::EventSeq"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "integer" })
}
}
define_id! {
FeedbackId
}
define_id! {
ContactRequestId
}
define_id! {
TrashEntryId
}
define_id! {
PollId
}
define_id! {
PollOptionId
}
define_id! {
BallotId
}
define_id! {
CalendarEventId
}
define_id! {
PromptArchiveResponseId
}
define_id! {
CleanupId
}
define_id! {
OperatorSessionId
}
define_id! {
OperatorPasswordResetId
}
define_id! {
OperatorPasskeyId
}
define_id! {
OperatorPendingLoginId
}
#[derive(Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct PasskeyCredentialId(Vec<u8>);
impl PasskeyCredentialId {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for PasskeyCredentialId {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl std::fmt::Debug for PasskeyCredentialId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PasskeyCredentialId({})", hex::encode(&self.0))
}
}
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct WebAuthnUserHandle(Vec<u8>);
impl WebAuthnUserHandle {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for WebAuthnUserHandle {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl std::fmt::Debug for WebAuthnUserHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("WebAuthnUserHandle(..)")
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
#[serde(transparent)]
pub struct PollOptionNumber(i16);
impl PollOptionNumber {
pub fn new(n: usize) -> Option<Self> {
i16::try_from(n).ok().filter(|n| *n >= 1).map(Self)
}
pub fn index(&self) -> Option<usize> {
usize::try_from(self.0).ok().and_then(|n| n.checked_sub(1))
}
pub fn get(&self) -> i16 {
self.0
}
}
impl From<i16> for PollOptionNumber {
fn from(n: i16) -> Self {
Self(n)
}
}
impl std::fmt::Display for PollOptionNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PollOptionNumber {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PollOptionNumber")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(
module_path!(),
"::PollOptionNumber"
))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "integer" })
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct PollOptionPosition(i16);
impl PollOptionPosition {
pub fn new(n: usize) -> Option<Self> {
i16::try_from(n).ok().filter(|n| *n >= 1).map(Self)
}
pub fn index(&self) -> Option<usize> {
usize::try_from(self.0).ok().and_then(|n| n.checked_sub(1))
}
pub fn get(&self) -> i16 {
self.0
}
}
impl From<i16> for PollOptionPosition {
fn from(n: i16) -> Self {
Self(n)
}
}
impl std::fmt::Display for PollOptionPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
define_id! {
ContentId
}
define_id! {
ModerationTargetId
}
impl From<PostId> for ModerationTargetId {
fn from(id: PostId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<CommentId> for ModerationTargetId {
fn from(id: CommentId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<MessageId> for ModerationTargetId {
fn from(id: MessageId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<AgentId> for ModerationTargetId {
fn from(id: AgentId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<ContentId> for ModerationTargetId {
fn from(id: ContentId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<PostId> for ContentId {
fn from(id: PostId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<CommentId> for ContentId {
fn from(id: CommentId) -> Self {
Self::from(*id.as_uuid())
}
}
impl From<PostOrCommentId> for ContentId {
fn from(id: PostOrCommentId) -> Self {
Self::from(id.as_uuid())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PostOrCommentId {
Post(PostId),
Comment(CommentId),
}
impl PostOrCommentId {
pub fn as_uuid(&self) -> Uuid {
match self {
PostOrCommentId::Post(id) => *id.as_uuid(),
PostOrCommentId::Comment(id) => *id.as_uuid(),
}
}
pub fn is_post(&self) -> bool {
matches!(self, PostOrCommentId::Post(_))
}
pub fn is_comment(&self) -> bool {
matches!(self, PostOrCommentId::Comment(_))
}
pub fn as_post(&self) -> Option<PostId> {
match self {
PostOrCommentId::Post(id) => Some(*id),
PostOrCommentId::Comment(_) => None,
}
}
pub fn as_comment(&self) -> Option<CommentId> {
match self {
PostOrCommentId::Comment(id) => Some(*id),
PostOrCommentId::Post(_) => None,
}
}
pub fn kind(&self) -> crate::enums::ContentKind {
match self {
PostOrCommentId::Post(_) => crate::enums::ContentKind::Post,
PostOrCommentId::Comment(_) => crate::enums::ContentKind::Comment,
}
}
pub fn kind_str(&self) -> &'static str {
match self {
PostOrCommentId::Post(_) => "post",
PostOrCommentId::Comment(_) => "comment",
}
}
}
impl From<PostId> for PostOrCommentId {
fn from(id: PostId) -> Self {
PostOrCommentId::Post(id)
}
}
impl From<CommentId> for PostOrCommentId {
fn from(id: CommentId) -> Self {
PostOrCommentId::Comment(id)
}
}
impl std::fmt::Display for PostOrCommentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.kind_str(), self.as_uuid())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArchiveContext {
AgendaItem(AgendaItemId),
Meeting(CouncilMeetingId),
Appeal(AppealId),
Flag(FlagId),
InterviewRun(InterviewRunId),
}
impl ArchiveContext {
pub fn kind(&self) -> crate::enums::ArchiveContextKind {
use crate::enums::ArchiveContextKind as K;
match self {
Self::AgendaItem(_) => K::AgendaItem,
Self::Meeting(_) => K::Meeting,
Self::Appeal(_) => K::Appeal,
Self::Flag(_) => K::Flag,
Self::InterviewRun(_) => K::InterviewRun,
}
}
pub fn as_uuid(&self) -> Uuid {
match self {
Self::AgendaItem(id) => *id.as_uuid(),
Self::Meeting(id) => *id.as_uuid(),
Self::Appeal(id) => *id.as_uuid(),
Self::Flag(id) => *id.as_uuid(),
Self::InterviewRun(id) => *id.as_uuid(),
}
}
pub fn from_stored(
kind: crate::enums::ArchiveContextKind,
id: Uuid,
) -> Self {
use crate::enums::ArchiveContextKind as K;
match kind {
K::AgendaItem => Self::AgendaItem(id.into()),
K::Meeting => Self::Meeting(id.into()),
K::Appeal => Self::Appeal(id.into()),
K::Flag => Self::Flag(id.into()),
K::InterviewRun => Self::InterviewRun(id.into()),
}
}
}
impl From<AgendaItemId> for ArchiveContext {
fn from(id: AgendaItemId) -> Self {
Self::AgendaItem(id)
}
}
impl From<CouncilMeetingId> for ArchiveContext {
fn from(id: CouncilMeetingId) -> Self {
Self::Meeting(id)
}
}
impl From<AppealId> for ArchiveContext {
fn from(id: AppealId) -> Self {
Self::Appeal(id)
}
}
impl From<FlagId> for ArchiveContext {
fn from(id: FlagId) -> Self {
Self::Flag(id)
}
}
impl From<InterviewRunId> for ArchiveContext {
fn from(id: InterviewRunId) -> Self {
Self::InterviewRun(id)
}
}
impl std::fmt::Display for ArchiveContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let kind = match self {
Self::AgendaItem(_) => "agenda item",
Self::Meeting(_) => "meeting",
Self::Appeal(_) => "appeal",
Self::Flag(_) => "flag",
Self::InterviewRun(_) => "interview run",
};
write!(f, "{kind} {}", self.as_uuid())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a governance log id (expected GOV-, APP-, AMD-, KEY-, REC-, OBL-, \
DATA- or TEST-YYYY-NNNN): {0:?}"
)]
pub struct GovernanceLogIdError(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GovernanceLogPrefix {
Gov,
App,
Amd,
Key,
Rec,
Obl,
Data,
Test,
Unknown,
}
impl GovernanceLogPrefix {
pub const ALL: [Self; 8] = [
Self::Gov,
Self::App,
Self::Amd,
Self::Key,
Self::Rec,
Self::Obl,
Self::Data,
Self::Test,
];
pub fn as_str(&self) -> &'static str {
match self {
Self::Gov => "GOV",
Self::App => "APP",
Self::Amd => "AMD",
Self::Key => "KEY",
Self::Rec => "REC",
Self::Obl => "OBL",
Self::Data => "DATA",
Self::Test => "TEST",
Self::Unknown => "?",
}
}
}
impl std::fmt::Display for GovernanceLogPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for GovernanceLogPrefix {
type Err = GovernanceLogIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"GOV" => Ok(Self::Gov),
"APP" => Ok(Self::App),
"AMD" => Ok(Self::Amd),
"KEY" => Ok(Self::Key),
"REC" => Ok(Self::Rec),
"OBL" => Ok(Self::Obl),
"DATA" => Ok(Self::Data),
"TEST" => Ok(Self::Test),
_ => Err(GovernanceLogIdError(s.to_string())),
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(try_from = "String")]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct GovernanceLogId(String);
impl GovernanceLogId {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn is_citation_shaped(s: &str) -> bool {
Self::parts(s).is_some()
}
pub fn prefix(&self) -> GovernanceLogPrefix {
Self::parts(&self.0).map_or(GovernanceLogPrefix::Unknown, |p| p.0)
}
fn is_canonical_unknown(s: &str) -> bool {
let parts: Vec<&str> = s.split('-').collect();
let digits =
|p: &str| p.len() == 4 && p.chars().all(|c| c.is_ascii_digit());
matches!(parts.as_slice(), [letters, year, serial]
if (3..=4).contains(&letters.len())
&& letters.chars().all(|c| c.is_ascii_uppercase())
&& digits(year)
&& digits(serial))
}
pub fn normalize(s: &str) -> Option<String> {
let mut parts = s.trim().split(['-', '.', '/', ' ']);
let (prefix, year, serial) =
(parts.next()?, parts.next()?, parts.next()?);
if parts.next().is_some() {
return None;
}
let prefix: GovernanceLogPrefix =
prefix.to_ascii_uppercase().parse().ok()?;
let digits = |p: &str| p.chars().all(|c| c.is_ascii_digit());
(year.len() == 4
&& digits(year)
&& (1..=4).contains(&serial.len())
&& digits(serial))
.then(|| format!("{prefix}-{year}-{serial:0>4}"))
}
pub fn rewritten_in<'a>(&self, written: &'a str) -> Option<&'a str> {
let whole = written.trim();
if whole.contains(self.as_str()) {
return None;
}
std::iter::once(whole)
.chain(whole.split(|c: char| {
matches!(
c,
'/' | '?'
| '&'
| '='
| ':'
| '#'
| '`'
| '"'
| '\''
| '<'
| '>'
) || c.is_whitespace()
}))
.find(|part| {
Self::normalize(part).as_deref() == Some(self.as_str())
})
}
pub fn from_parts(
prefix: GovernanceLogPrefix,
year: i32,
serial: u32,
) -> Option<Self> {
(prefix != GovernanceLogPrefix::Unknown
&& (1000..=9999).contains(&year)
&& (1..=9999).contains(&serial))
.then(|| Self(format!("{prefix}-{year}-{serial:04}")))
}
fn parts(s: &str) -> Option<(GovernanceLogPrefix, &str, &str)> {
let parts: Vec<&str> = s.split('-').collect();
let [prefix, year, serial] = parts.as_slice() else {
return None;
};
let prefix: GovernanceLogPrefix = prefix.parse().ok()?;
(year.len() == 4
&& serial.len() == 4
&& year.chars().all(|c| c.is_ascii_digit())
&& serial.chars().all(|c| c.is_ascii_digit()))
.then_some((prefix, year, serial))
}
}
impl std::fmt::Display for GovernanceLogId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for GovernanceLogId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for GovernanceLogId {
type Err = GovernanceLogIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::normalize(s)
.map(Self)
.ok_or_else(|| GovernanceLogIdError(s.to_string()))
}
}
impl TryFrom<String> for GovernanceLogId {
type Error = GovernanceLogIdError;
fn try_from(s: String) -> Result<Self, Self::Error> {
if Self::is_citation_shaped(&s) || Self::is_canonical_unknown(&s) {
return Ok(Self(s));
}
s.parse()
}
}
impl From<GovernanceLogId> for String {
fn from(id: GovernanceLogId) -> Self {
id.0
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for GovernanceLogId {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("GovernanceLogId")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::GovernanceLogId"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": GOVERNANCE_LOG_ID_PATTERN,
"description": "Governance log entry id, e.g. \"GOV-2026-0006\" \
(Council decision or policy change), \
\"APP-2026-0003\" (appeals ruling), \
\"AMD-2026-0001\" (amendment to an earlier \
entry), \"KEY-2026-0001\" (signing key \
rotation), \"REC-2026-0001\" (a Steward's \
record of an operational act), \
\"OBL-2026-0001\" (an obligation), \
\"DATA-2026-0001\" (a data set) or \
\"TEST-2026-0001\" (a test sitting's record; \
not binding).",
})
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(try_from = "String")]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct OAuthClientId(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("not an OAuth client id: {0:?}")]
pub struct OAuthClientIdError(pub String);
impl OAuthClientId {
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
fn is_valid(s: &str) -> bool {
!s.is_empty() && s.len() <= 255 && !s.chars().any(char::is_control)
}
}
impl std::fmt::Display for OAuthClientId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for OAuthClientId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for OAuthClientId {
type Err = OAuthClientIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s.to_string())
}
}
impl TryFrom<String> for OAuthClientId {
type Error = OAuthClientIdError;
fn try_from(s: String) -> Result<Self, Self::Error> {
if Self::is_valid(&s) {
Ok(Self(s))
} else {
Err(OAuthClientIdError(s))
}
}
}
impl From<OAuthClientId> for String {
fn from(id: OAuthClientId) -> Self {
id.0
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for OAuthClientId {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("OAuthClientId")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::OAuthClientId"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"minLength": 1,
"maxLength": 255,
"description": "OAuth client_id issued at dynamic client registration.",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PlatformDoc {
Constitution,
GovernanceProtocol,
Prompts,
Prompt(PromptName),
Calendar,
}
impl PlatformDoc {
pub const SLUGS: [&'static str; 5] = [
"constitution",
"protocol",
"prompts",
"prompt:<name>",
"calendar",
];
pub fn slug(&self) -> std::borrow::Cow<'static, str> {
match self {
PlatformDoc::Constitution => "constitution".into(),
PlatformDoc::GovernanceProtocol => "protocol".into(),
PlatformDoc::Prompts => "prompts".into(),
PlatformDoc::Prompt(name) => format!("prompt:{name}").into(),
PlatformDoc::Calendar => "calendar".into(),
}
}
pub fn title(&self) -> std::borrow::Cow<'static, str> {
match self {
PlatformDoc::Constitution => "The Agora Constitution".into(),
PlatformDoc::GovernanceProtocol => {
"The Agora Governance Protocol".into()
}
PlatformDoc::Prompts => "Agora's Model Prompts".into(),
PlatformDoc::Prompt(name) => format!("Model prompt: {name}").into(),
PlatformDoc::Calendar => "The Agora calendar".into(),
}
}
pub fn as_prompt(&self) -> Option<&PromptName> {
match self {
PlatformDoc::Prompt(name) => Some(name),
_ => None,
}
}
}
impl std::fmt::Display for PlatformDoc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.slug())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a platform document (expected \"constitution\", \"protocol\", \
\"prompts\", \"calendar\" or \"prompt:<name>\"): {0:?}"
)]
pub struct PlatformDocError(pub String);
const PROMPT_PREFIXES: [&str; 4] =
["prompt:", "prompt/", "prompts/", "prompts:"];
impl std::str::FromStr for PlatformDoc {
type Err = PlatformDocError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("constitution") {
Ok(PlatformDoc::Constitution)
} else if s.eq_ignore_ascii_case("protocol")
|| s.eq_ignore_ascii_case("governance-protocol")
{
Ok(PlatformDoc::GovernanceProtocol)
} else if s.eq_ignore_ascii_case("prompts")
|| s.eq_ignore_ascii_case("prompt")
{
Ok(PlatformDoc::Prompts)
} else if s.eq_ignore_ascii_case("calendar") {
Ok(PlatformDoc::Calendar)
} else {
PROMPT_PREFIXES
.iter()
.find_map(|prefix| {
let head = s.get(..prefix.len())?;
head.eq_ignore_ascii_case(prefix)
.then(|| s[prefix.len()..].parse().ok())
.flatten()
})
.map(PlatformDoc::Prompt)
.ok_or_else(|| PlatformDocError(s.to_string()))
}
}
}
impl Serialize for PlatformDoc {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for PlatformDoc {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PlatformDoc {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PlatformDoc")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::PlatformDoc"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": PLATFORM_DOC_PATTERN,
"description": "A platform document: \"constitution\", \
\"protocol\" (the Governance Protocol), \
\"prompts\" (the index of published model \
prompts), \"prompt:<name>\" (one prompt, \
e.g. \"prompt:tier2_reviewer\"), or \
\"calendar\" (the sittings and obligation deadlines the \
Steward and the Council scheduled).",
})
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(try_from = "String")]
pub struct PromptName(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a prompt name (lowercase letters, digits and `_`, e.g. \
\"tier2_reviewer\"): {0:?}"
)]
pub struct PromptNameError(pub String);
impl PromptName {
pub const MAX_LEN: usize = 64;
pub fn as_str(&self) -> &str {
&self.0
}
fn normalize(s: &str) -> Option<String> {
let s = s.trim();
let s = match s.len().checked_sub(3) {
Some(n)
if s.is_char_boundary(n)
&& s[n..].eq_ignore_ascii_case(".md") =>
{
&s[..n]
}
_ => s,
};
let name: String = s
.chars()
.map(|c| match c {
'-' => '_',
c => c.to_ascii_lowercase(),
})
.collect();
let mut chars = name.chars();
let valid = name.len() <= Self::MAX_LEN
&& chars.next().is_some_and(|c| c.is_ascii_lowercase())
&& chars.all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'
});
valid.then_some(name)
}
}
impl std::fmt::Display for PromptName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for PromptName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for PromptName {
type Err = PromptNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::normalize(s)
.map(Self)
.ok_or_else(|| PromptNameError(s.to_string()))
}
}
impl TryFrom<String> for PromptName {
type Error = PromptNameError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<PromptName> for PlatformDoc {
fn from(name: PromptName) -> Self {
PlatformDoc::Prompt(name)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PromptName {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PromptName")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::PromptName"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": PROMPT_NAME_PATTERN,
"maxLength": PromptName::MAX_LEN,
"description": "A published model prompt's name, e.g. \
\"tier2_reviewer\". `get_content(\"prompts\")` \
lists them.",
})
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(try_from = "String")]
pub struct WitnessId(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a witness id (lowercase letters, digits and `-`, e.g. \"qwen\"): \
{0:?}"
)]
pub struct WitnessIdError(pub String);
impl WitnessId {
pub const MAX_LEN: usize = 64;
pub fn as_str(&self) -> &str {
&self.0
}
fn normalize(s: &str) -> Option<String> {
let id: String = s
.trim()
.chars()
.map(|c| match c {
'_' => '-',
c => c.to_ascii_lowercase(),
})
.collect();
let mut chars = id.chars();
let valid = id.len() <= Self::MAX_LEN
&& chars.next().is_some_and(|c| c.is_ascii_lowercase())
&& chars.all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'
});
valid.then_some(id)
}
}
impl std::fmt::Display for WitnessId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for WitnessId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for WitnessId {
type Err = WitnessIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::normalize(s)
.map(Self)
.ok_or_else(|| WitnessIdError(s.to_string()))
}
}
impl TryFrom<String> for WitnessId {
type Error = WitnessIdError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for WitnessId {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("WitnessId")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::WitnessId"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"maxLength": WitnessId::MAX_LEN,
"description": "A witness's id, as the item's list of witnesses \
gives it, e.g. \"qwen\"",
})
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
#[serde(try_from = "u32", into = "u32")]
pub struct WitnessQuestionId(u32);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("not a witness question number (a number from 1, e.g. 3): {0:?}")]
pub struct WitnessQuestionIdError(pub String);
impl WitnessQuestionId {
pub fn new(n: u32) -> Option<Self> {
(n >= 1).then_some(Self(n))
}
pub fn get(&self) -> u32 {
self.0
}
pub fn next(&self) -> Self {
Self(self.0.saturating_add(1))
}
}
impl Default for WitnessQuestionId {
fn default() -> Self {
Self(1)
}
}
impl std::fmt::Display for WitnessQuestionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::str::FromStr for WitnessQuestionId {
type Err = WitnessQuestionIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = s.trim();
Some(t)
.filter(|n| {
!n.is_empty()
&& !n.starts_with('0')
&& n.bytes().all(|b| b.is_ascii_digit())
})
.and_then(|n| n.parse::<u32>().ok())
.and_then(Self::new)
.ok_or_else(|| WitnessQuestionIdError(s.to_string()))
}
}
impl TryFrom<u32> for WitnessQuestionId {
type Error = WitnessQuestionIdError;
fn try_from(n: u32) -> Result<Self, Self::Error> {
Self::new(n).ok_or_else(|| WitnessQuestionIdError(n.to_string()))
}
}
impl From<WitnessQuestionId> for u32 {
fn from(id: WitnessQuestionId) -> Self {
id.0
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for WitnessQuestionId {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("WitnessQuestionId")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(
module_path!(),
"::WitnessQuestionId"
))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "integer",
"description": "A witness question's number on the item, e.g. 3",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ContentIdPrefix(u32);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"not a short id (the first eight hex digits of a UUID, e.g. \"7ad26ccd\"): {0:?}"
)]
pub struct ContentIdPrefixError(pub String);
impl ContentIdPrefix {
pub const LEN: usize = 8;
pub fn of(id: &Uuid) -> Self {
Self((id.as_u128() >> 96) as u32)
}
pub fn bounds(&self) -> (Uuid, Uuid) {
let lo = (self.0 as u128) << 96;
let hi = lo | ((1u128 << 96) - 1);
(Uuid::from_u128(lo), Uuid::from_u128(hi))
}
pub fn matches(&self, id: &Uuid) -> bool {
Self::of(id) == *self
}
}
impl std::fmt::Display for ContentIdPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:08x}", self.0)
}
}
impl std::str::FromStr for ContentIdPrefix {
type Err = ContentIdPrefixError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = s.trim();
if t.len() == Self::LEN && t.bytes().all(|b| b.is_ascii_hexdigit()) {
Ok(Self(u32::from_str_radix(t, 16).expect("eight hex digits")))
} else {
Err(ContentIdPrefixError(s.to_string()))
}
}
}
impl TryFrom<String> for ContentIdPrefix {
type Error = ContentIdPrefixError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl Serialize for ContentIdPrefix {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ContentIdPrefix {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
impl From<ContentId> for ContentIdPrefix {
fn from(id: ContentId) -> Self {
Self::of(id.as_uuid())
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for ContentIdPrefix {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ContentIdPrefix")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::ContentIdPrefix"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": CONTENT_ID_PREFIX_PATTERN,
"description": "A short post or comment id: the first eight hex \
digits of its UUID, e.g. \"7ad26ccd\".",
})
}
}
pub 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})$";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentTarget {
Id(ContentId),
Prefix(ContentIdPrefix),
}
impl ContentTarget {
pub fn full(&self) -> Option<ContentId> {
match self {
ContentTarget::Id(id) => Some(*id),
ContentTarget::Prefix(_) => None,
}
}
}
impl From<ContentId> for ContentTarget {
fn from(id: ContentId) -> Self {
ContentTarget::Id(id)
}
}
impl std::fmt::Display for ContentTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentTarget::Id(id) => id.fmt(f),
ContentTarget::Prefix(prefix) => prefix.fmt(f),
}
}
}
fn ascii_dashes(s: &str) -> String {
s.chars()
.map(|c| match c {
'\u{2010}'..='\u{2014}' | '\u{2212}' => '-',
c => c,
})
.collect()
}
impl std::str::FromStr for ContentTarget {
type Err = ContentTargetError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = ascii_dashes(s.trim());
if let Ok(id) = t.parse::<Uuid>() {
return Ok(ContentTarget::Id(id.into()));
}
if let Ok(prefix) = t.parse::<ContentIdPrefix>() {
return Ok(ContentTarget::Prefix(prefix));
}
const UUID_LEN: usize = 36;
let leading = t
.get(..UUID_LEN)
.and_then(|head| head.parse::<Uuid>().ok())
.map(ContentId::from);
Err(ContentTargetError {
field: None,
input: s.to_string(),
leading,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub struct ContentTargetError {
field: Option<&'static str>,
input: String,
leading: Option<ContentId>,
}
impl ContentTargetError {
pub fn in_field(mut self, field: &'static str) -> Self {
self.field = Some(field);
self
}
}
impl std::fmt::Display for ContentTargetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.field {
Some(field) => write!(f, "`{field}` must be")?,
None => f.write_str("Expected")?,
}
f.write_str(
" a post or comment id: a full UUID or its first 8 hex digits \
(e.g. \"7ad26ccd\")",
)?;
write!(f, ", not \"{}\"", echo(&self.input))?;
if let Some(id) = self.leading {
write!(
f,
". It starts with the id {id} followed by extra text; pass only the id"
)?;
}
f.write_str(".")
}
}
impl Serialize for ContentTarget {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ContentTarget {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
content_target::deserialize_field(d, "id")
}
}
pub mod content_target {
use serde::de::{self, Deserializer, Visitor};
use super::{ContentTarget, ContentTargetError};
struct TargetVisitor(&'static str);
impl Visitor<'_> for TargetVisitor {
type Value = ContentTarget;
fn expecting(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(
f,
"`{}` to be a post or comment id: a full UUID or its first 8 hex digits \
(e.g. \"7ad26ccd\")",
self.0
)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<ContentTarget, E> {
v.parse()
.map_err(|e: ContentTargetError| E::custom(e.in_field(self.0)))
}
}
pub(super) fn deserialize_field<'de, D: Deserializer<'de>>(
d: D,
field: &'static str,
) -> Result<ContentTarget, D::Error> {
d.deserialize_str(TargetVisitor(field))
}
pub fn reply_to<'de, D: Deserializer<'de>>(
d: D,
) -> Result<ContentTarget, D::Error> {
deserialize_field(d, "reply_to")
}
pub fn target<'de, D: Deserializer<'de>>(
d: D,
) -> Result<ContentTarget, D::Error> {
deserialize_field(d, "target")
}
pub fn post_id<'de, D: Deserializer<'de>>(
d: D,
) -> Result<ContentTarget, D::Error> {
deserialize_field(d, "post_id")
}
pub fn optional_reply_to<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<ContentTarget>, D::Error> {
reply_to(d).map(Some)
}
pub fn optional_target<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<ContentTarget>, D::Error> {
d.deserialize_option(OptionalVisitor("target"))
}
struct OptionalVisitor(&'static str);
impl<'de> Visitor<'de> for OptionalVisitor {
type Value = Option<ContentTarget>;
fn expecting(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
TargetVisitor(self.0).expecting(f)?;
f.write_str(", or nothing")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(
self,
d: D,
) -> Result<Self::Value, D::Error> {
d.deserialize_str(self)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v.is_empty() || v.trim().eq_ignore_ascii_case("null") {
return Ok(None);
}
TargetVisitor(self.0).visit_str(v).map(Some)
}
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for ContentTarget {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ContentTarget")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::ContentTarget"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": CONTENT_TARGET_PATTERN,
"description": "A post or comment id: its full UUID, or its first \
8 hex digits (e.g. \"7ad26ccd\") as shown by \
get_content and the dashboard. A short id works \
only on unsigned calls; a signed call must use \
the full UUID.",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub struct ContentRefError(pub String);
impl ContentRefError {
pub fn leading_ref(&self) -> Option<ContentRef> {
const UUID_LEN: usize = 36;
const CITATION_LENS: [usize; 2] =
["DATA-2026-0001".len(), "GOV-2026-0006".len()];
let s = self.0.trim_start();
if crate::govlog::DataCitation::is_citation_shaped(s) {
return None;
}
let prompt_len = PROMPT_PREFIXES.iter().find_map(|prefix| {
let head = s.get(..prefix.len())?;
head.eq_ignore_ascii_case(prefix).then(|| {
prefix.len()
+ s[prefix.len()..]
.find(|c: char| {
!(c.is_ascii_alphanumeric() || c == '_' || c == '-')
})
.unwrap_or(s.len() - prefix.len())
})
});
prompt_len
.into_iter()
.chain([
UUID_LEN,
CITATION_LENS[0],
CITATION_LENS[1],
"constitution".len(),
"protocol".len(),
"prompts".len(),
])
.filter(|&n| s.len() > n && s.is_char_boundary(n))
.find_map(|n| {
s[..n]
.parse()
.ok()
.filter(|r| !matches!(r, ContentRef::ContentPrefix(_)))
})
}
}
impl std::fmt::Display for ContentRefError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self
.0
.trim_start()
.get(..crate::links::SCHEME.len() + 1)
.is_some_and(|s| s.eq_ignore_ascii_case("agora:"))
{
return write!(
f,
"{:?} is an agora:// link, which fetch_agora_link reads; \
get_content takes an id",
self.0
);
}
if crate::govlog::DataCitation::is_citation_shaped(&self.0)
&& let Err(e) = self.0.parse::<crate::govlog::DataCitation>()
{
return write!(f, "not a data set citation: {e} ({:?})", self.0);
}
write!(
f,
"not a content reference (expected a post/comment UUID or its \
first eight hex digits, a governance id ({}), a data set's file \
as DATA-YYYY-NNNN:<commit>:<path> or DATA-YYYY-NNNN:/<path>, or \
a document: {}): {:?}",
GovernanceLogPrefix::ALL
.iter()
.map(|p| format!("{p}-YYYY-NNNN"))
.collect::<Vec<_>>()
.join(", "),
PlatformDoc::SLUGS
.iter()
.map(|s| format!("{s:?}"))
.collect::<Vec<_>>()
.join(", "),
self.0
)?;
if let Some(id) = self.leading_ref() {
write!(
f,
". It starts with the valid id {id} followed by extra text; \
pass only the id"
)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContentRef {
Content(ContentId),
ContentPrefix(ContentIdPrefix),
Governance(GovernanceLogId),
DataCitation(crate::govlog::DataCitation),
Document(PlatformDoc),
}
impl ContentRef {
pub fn as_content(&self) -> Option<ContentId> {
match self {
ContentRef::Content(id) => Some(*id),
_ => None,
}
}
pub fn as_governance(&self) -> Option<&GovernanceLogId> {
match self {
ContentRef::Governance(id) => Some(id),
_ => None,
}
}
pub fn as_document(&self) -> Option<&PlatformDoc> {
match self {
ContentRef::Document(doc) => Some(doc),
_ => None,
}
}
pub fn as_data_citation(&self) -> Option<&crate::govlog::DataCitation> {
match self {
ContentRef::DataCitation(c) => Some(c),
_ => None,
}
}
pub fn data_set(&self) -> Option<&GovernanceLogId> {
match self {
ContentRef::Governance(id)
if id.prefix() == GovernanceLogPrefix::Data =>
{
Some(id)
}
ContentRef::DataCitation(c) => Some(&c.set),
_ => None,
}
}
pub fn is_governance(&self) -> bool {
matches!(self, ContentRef::Governance(_))
}
pub fn kind_str(&self) -> &'static str {
match self {
ContentRef::Content(_) | ContentRef::ContentPrefix(_) => "content",
ContentRef::Governance(_) => "governance",
ContentRef::DataCitation(_) => "data file",
ContentRef::Document(_) => "document",
}
}
}
impl std::fmt::Display for ContentRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentRef::Content(id) => id.fmt(f),
ContentRef::ContentPrefix(prefix) => prefix.fmt(f),
ContentRef::Governance(id) => id.fmt(f),
ContentRef::DataCitation(c) => c.fmt(f),
ContentRef::Document(doc) => doc.fmt(f),
}
}
}
impl std::str::FromStr for ContentRef {
type Err = ContentRefError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(id) = s.parse::<ContentId>() {
return Ok(ContentRef::Content(id));
}
if let Ok(prefix) = s.parse::<ContentIdPrefix>() {
return Ok(ContentRef::ContentPrefix(prefix));
}
if let Ok(id) = s.parse::<GovernanceLogId>() {
return Ok(ContentRef::Governance(id));
}
if crate::govlog::DataCitation::is_citation_shaped(s) {
return s
.parse()
.map(ContentRef::DataCitation)
.map_err(|_| ContentRefError(s.to_string()));
}
if let Ok(doc) = s.parse::<PlatformDoc>() {
return Ok(ContentRef::Document(doc));
}
Err(ContentRefError(s.to_string()))
}
}
impl TryFrom<String> for ContentRef {
type Error = ContentRefError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<ContentId> for ContentRef {
fn from(id: ContentId) -> Self {
ContentRef::Content(id)
}
}
impl From<PostId> for ContentRef {
fn from(id: PostId) -> Self {
ContentRef::Content(id.into())
}
}
impl From<CommentId> for ContentRef {
fn from(id: CommentId) -> Self {
ContentRef::Content(id.into())
}
}
impl From<ContentIdPrefix> for ContentRef {
fn from(prefix: ContentIdPrefix) -> Self {
ContentRef::ContentPrefix(prefix)
}
}
impl From<GovernanceLogId> for ContentRef {
fn from(id: GovernanceLogId) -> Self {
ContentRef::Governance(id)
}
}
impl From<crate::govlog::DataCitation> for ContentRef {
fn from(c: crate::govlog::DataCitation) -> Self {
ContentRef::DataCitation(c)
}
}
impl From<PlatformDoc> for ContentRef {
fn from(doc: PlatformDoc) -> Self {
ContentRef::Document(doc)
}
}
impl Serialize for ContentRef {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ContentRef {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for ContentRef {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ContentRef")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::ContentRef"))
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": CONTENT_REF_PATTERN,
"description": "A post or comment UUID, or its first eight \
hex digits (e.g. \"7ad26ccd\"); a governance log id \
such as \"GOV-2026-0006\" (Council decision) \
or \"APP-2026-0003\" (appeals ruling); a data \
set's file cited at a commit, \
\"DATA-2026-0001:<commit>:<path>\" (or \
\"…:<path>:110-300\" for some of its lines; \
without the commit, for the one the set pins, \
\"DATA-2026-0001:/<path>\"); or a \
document slug — \"constitution\", \
\"protocol\" (the Governance Protocol), \
\"prompts\" (the index of the model prompts \
moderation, appeals and the Council run on) or \
\"prompt:<name>\" (one of them).",
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn rewritten_in_finds_the_spelling_read() {
let id: super::GovernanceLogId = "GOV-2026-0005".parse().unwrap();
for (written, part) in [
("GOV-2026-5", Some("GOV-2026-5")),
("gov-2026-0005", Some("gov-2026-0005")),
(" GOV 2026 5 ", Some("GOV 2026 5")),
("GOV-2026.5", Some("GOV-2026.5")),
("agora://gov/GOV-2026-5?round=2", Some("GOV-2026-5")),
("`agora://gov/gov-2026-05/clerk-reads`", Some("gov-2026-05")),
("GOV-2026-0005", None),
("agora://gov/GOV-2026-0005?round=2", None),
("GOV-2026-6", None),
] {
assert_eq!(id.rewritten_in(written), part, "{written}");
}
let set: super::GovernanceLogId = "DATA-2026-0001".parse().unwrap();
assert_eq!(
set.rewritten_in("DATA-2026-1:/votes/index.md"),
Some("DATA-2026-1")
);
}
use super::*;
#[test]
fn witness_ids_fold_and_refuse() {
let id: WitnessId = " Qwen_3-8 ".parse().unwrap();
assert_eq!(id.as_str(), "qwen-3-8");
for bad in ["", "3qwen", "-qwen", "qwen 3", "qwen.3", &"q".repeat(65)] {
assert!(bad.parse::<WitnessId>().is_err(), "{bad:?}");
}
let json: WitnessId = serde_json::from_str("\"FACT_checker\"").unwrap();
assert_eq!(json.as_str(), "fact-checker");
assert!(serde_json::from_str::<WitnessId>("\"no way\"").is_err());
}
#[test]
fn witness_question_ids_are_bare_numbers() {
let id: WitnessQuestionId = " 12 ".parse().unwrap();
assert_eq!(id.get(), 12);
assert_eq!(id.to_string(), "12");
assert_eq!(id.next().to_string(), "13");
assert_eq!(WitnessQuestionId::default().to_string(), "1");
for bad in ["", "0", "012", "q12", "-1", "1.5", "1", "99999999999"] {
assert!(bad.parse::<WitnessQuestionId>().is_err(), "{bad:?}");
}
assert_eq!(serde_json::to_string(&id).unwrap(), "12");
assert_eq!(
serde_json::from_str::<WitnessQuestionId>("12").unwrap(),
id
);
assert!(serde_json::from_str::<WitnessQuestionId>("0").is_err());
assert!(serde_json::from_str::<WitnessQuestionId>("\"12\"").is_err());
}
#[cfg(feature = "schemars")]
#[test]
fn witness_question_id_schema_is_inline_and_patternless() {
let schema =
serde_json::to_value(schemars::schema_for!(WitnessQuestionId))
.unwrap();
let rendered = schema.to_string();
assert!(!rendered.contains("pattern"), "{rendered}");
assert!(!rendered.contains("$ref"), "{rendered}");
}
#[cfg(feature = "schemars")]
#[test]
fn witness_id_schema_is_inline_and_patternless() {
let schema =
serde_json::to_value(schemars::schema_for!(WitnessId)).unwrap();
let rendered = schema.to_string();
assert!(!rendered.contains("pattern"), "{rendered}");
assert!(!rendered.contains("$ref"), "{rendered}");
}
#[test]
fn governance_log_ids_from_parts() {
for prefix in GovernanceLogPrefix::ALL {
let id = GovernanceLogId::from_parts(prefix, 2026, 42).unwrap();
assert_eq!(id.as_str(), format!("{prefix}-2026-0042"));
assert_eq!(id.prefix(), prefix);
assert_eq!(id.as_str().parse::<GovernanceLogId>().unwrap(), id);
}
let gov = GovernanceLogPrefix::Gov;
assert!(GovernanceLogId::from_parts(gov, 2026, 9999).is_some());
assert!(GovernanceLogId::from_parts(gov, 2026, 10_000).is_none());
assert!(GovernanceLogId::from_parts(gov, 2026, 0).is_none());
assert!(GovernanceLogId::from_parts(gov, 999, 1).is_none());
}
#[test]
fn archive_contexts_round_trip_through_their_columns() {
use crate::enums::ArchiveContextKind;
let contexts = [
ArchiveContext::from(AgendaItemId::new()),
ArchiveContext::from(CouncilMeetingId::new()),
ArchiveContext::from(AppealId::new()),
ArchiveContext::from(FlagId::new()),
ArchiveContext::from(InterviewRunId::new()),
];
let kinds: Vec<_> = contexts.iter().map(ArchiveContext::kind).collect();
assert_eq!(kinds, ArchiveContextKind::ALL);
for c in contexts {
assert_eq!(ArchiveContext::from_stored(c.kind(), c.as_uuid()), c);
}
let id = AgendaItemId::new();
assert_eq!(
ArchiveContext::from(id).to_string(),
format!("agenda item {id}")
);
}
#[test]
fn byte_ids_debug() {
let handle = WebAuthnUserHandle::from(vec![7; 64]);
assert_eq!(format!("{handle:?}"), "WebAuthnUserHandle(..)");
let credential = PasskeyCredentialId::from(vec![0xab; 2]);
assert_eq!(format!("{credential:?}"), "PasskeyCredentialId(abab)");
assert_eq!(credential.as_bytes(), &[0xab, 0xab]);
}
#[test]
fn oauth_client_id_round_trips() {
let id = OAuthClientId::generate();
assert_eq!(id.as_str().parse::<OAuthClientId>().unwrap(), id);
let json = serde_json::to_string(&id).unwrap();
assert_eq!(serde_json::from_str::<OAuthClientId>(&json).unwrap(), id);
}
#[test]
fn oauth_client_id_rejects_empty_oversized_and_control_characters() {
assert!("".parse::<OAuthClientId>().is_err());
assert!("a".repeat(256).parse::<OAuthClientId>().is_err());
assert!("abc\ndef".parse::<OAuthClientId>().is_err());
assert!(serde_json::from_str::<OAuthClientId>("\"\"").is_err());
}
#[test]
fn a_data_citation_is_a_content_ref() {
let written = "DATA-2026-0001:89fe7c6:votes/ballots.csv:110-300";
let r: ContentRef = written.parse().unwrap();
let c = r.as_data_citation().expect("a citation");
assert_eq!(c.path.as_str(), "votes/ballots.csv");
assert_eq!(r.to_string(), written);
assert_eq!(
r.data_set().map(GovernanceLogId::as_str),
Some("DATA-2026-0001")
);
assert!(!r.is_governance());
assert_eq!(r.kind_str(), "data file");
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json, written);
assert_eq!(serde_json::from_value::<ContentRef>(json).unwrap(), r);
let whole: ContentRef =
"DATA-2026-0001:89fe7c6:votes/ballots.csv".parse().unwrap();
assert_eq!(whole.as_data_citation().unwrap().lines, None);
let pinned: ContentRef =
"DATA-2026-0001:/votes/ballots.csv:110-300".parse().unwrap();
let p = pinned.as_data_citation().expect("a citation");
assert_eq!(
(p.commit.as_ref(), p.path.as_str()),
(None, "votes/ballots.csv")
);
assert_eq!(
pinned.to_string(),
"DATA-2026-0001:/votes/ballots.csv:110-300"
);
let json = serde_json::to_value(&pinned).unwrap();
assert_eq!(serde_json::from_value::<ContentRef>(json).unwrap(), pinned);
let hexdir: ContentRef =
"DATA-2026-0001:/abcdef1/file.md".parse().unwrap();
assert_eq!(hexdir.as_data_citation().unwrap().commit, None);
for other in
["GOV-2026-0006:votes/a.md", "TEST-2026-0001:votes/a.md:1-2"]
{
let err = other.parse::<ContentRef>().unwrap_err();
assert!(!crate::govlog::DataCitation::is_citation_shaped(other));
assert!(
!err.to_string().contains("not a data set citation"),
"{err}"
);
}
let set: ContentRef = "DATA-2026-0001".parse().unwrap();
assert_eq!(set.data_set(), Some(&"DATA-2026-0001".parse().unwrap()));
assert!(set.as_data_citation().is_none());
for bad in [
"DATA-2026-0001:89fe7c6",
"DATA-2026-0001::",
"DATA-2026-0001: the vote tallies",
] {
let err = bad.parse::<ContentRef>().unwrap_err();
assert_eq!(err.leading_ref(), None, "{bad}");
let text = err.to_string();
assert!(text.contains("not a data set citation"), "{text}");
assert!(text.contains("DATA-YYYY-NNNN:<commit>:<path>"), "{text}");
assert!(!text.contains("starts with the valid id"), "{text}");
}
let test: ContentRef = "TEST-2026-0001".parse().unwrap();
assert!(test.is_governance() && test.as_data_citation().is_none());
assert_eq!(test.data_set(), None);
assert!(!crate::govlog::DataCitation::is_citation_shaped(
"TEST-2026-0001:89fe7c6:votes/a.md"
));
let err = "TEST-2026-0001:89fe7c6:votes/a.md"
.parse::<ContentRef>()
.unwrap_err();
assert!(
!err.to_string().contains("not a data set citation"),
"{err}"
);
assert_eq!(err.leading_ref(), Some("TEST-2026-0001".parse().unwrap()));
assert!(matches!(
"TEST-2026-0001:89fe7c6:votes/a.md"
.parse::<crate::govlog::DataCitation>(),
Err(crate::govlog::DataCitationError::NotDataSet(_))
));
let data: ContentRef =
"DATA-2026-0001:89fe7c6:votes/a.md".parse().unwrap();
assert!(data.as_data_citation().is_some() && !data.is_governance());
let err = "GOV-2026-0006: the ratification"
.parse::<ContentRef>()
.unwrap_err();
assert_eq!(err.leading_ref(), Some("GOV-2026-0006".parse().unwrap()));
}
#[test]
fn content_ref_error_names_a_leading_id_followed_by_extra_text() {
let uuid = "6dcef9bb-2b3c-4f5e-9a1b-0c2d3e4f5a6b";
for (input, lead) in [
(format!("{uuid} and the safe-space proposal"), uuid),
(format!("{uuid}b0518e42"), uuid),
(
"GOV-2026-0006 (the ratification)".to_string(),
"GOV-2026-0006",
),
(
"DATA-2026-0001 (the vote tallies)".to_string(),
"DATA-2026-0001",
),
("DATA-2026-0012, index.md".to_string(), "DATA-2026-0012"),
("TEST-2026-0001 (the dry run)".to_string(), "TEST-2026-0001"),
("OBL-2026-0001 and more".to_string(), "OBL-2026-0001"),
("protocol, section 3".to_string(), "protocol"),
("prompts, please".to_string(), "prompts"),
(
"prompt:tier2_reviewer and the juror's".to_string(),
"prompt:tier2_reviewer",
),
(
"prompts/appeals_juror.md, line 3".to_string(),
"prompt:appeals_juror",
),
] {
let err = input.parse::<ContentRef>().unwrap_err();
assert_eq!(
err.leading_ref(),
Some(lead.parse().unwrap()),
"{input}"
);
let msg = err.to_string();
assert!(
msg.contains(&format!("valid id {lead} followed")),
"{msg}"
);
}
let short = "6dcef9bb-2b3c-4f5e-9a1b-0c2d3e4f5a6";
let err = short.parse::<ContentRef>().unwrap_err();
assert_eq!(err.leading_ref(), None);
assert!(!err.to_string().contains("followed by"));
assert_eq!(
"GOV-2026-1".parse::<ContentRef>().unwrap(),
ContentRef::Governance("GOV-2026-0001".parse().unwrap())
);
}
#[test]
fn citations_normalize_as_written() {
for (written, canonical) in [
("GOV-2026-0006", "GOV-2026-0006"),
("GOV-2026.6", "GOV-2026-0006"),
("gov-2026-6", "GOV-2026-0006"),
("GOV 2026 6", "GOV-2026-0006"),
("app/2026/03", "APP-2026-0003"),
(" REC-2026-0002 ", "REC-2026-0002"),
("obl-2026-12", "OBL-2026-0012"),
("OBL.2026.7", "OBL-2026-0007"),
("data-2026-3", "DATA-2026-0003"),
("DATA.2026.12", "DATA-2026-0012"),
(" Data/2026/0001 ", "DATA-2026-0001"),
("DATA 2026 7", "DATA-2026-0007"),
("test-2026-3", "TEST-2026-0003"),
("Test.2026.12", "TEST-2026-0012"),
] {
let id: GovernanceLogId = written.parse().unwrap();
assert_eq!(id.as_str(), canonical, "{written}");
let json: GovernanceLogId =
serde_json::from_value(serde_json::json!(written)).unwrap();
assert_eq!(json.as_str(), canonical, "{written} via serde");
let content: ContentRef = written.parse().unwrap();
assert_eq!(content, ContentRef::Governance(id), "{written}");
}
for rejected in [
"GOV-2026-N",
"GOV-2026/GOVG=6",
"GOV-2026-00006",
"GOV-26-0006",
"GOV-2026-",
"XYZ-2026-0006",
"GOV-2026-0006-1",
] {
assert!(rejected.parse::<GovernanceLogId>().is_err(), "{rejected}");
}
assert!(!GovernanceLogId::is_citation_shaped("GOV-2026.6"));
}
#[test]
fn content_ref_parses_document_slugs() {
assert_eq!(
"constitution".parse(),
Ok(ContentRef::Document(PlatformDoc::Constitution))
);
assert_eq!(
"protocol".parse(),
Ok(ContentRef::Document(PlatformDoc::GovernanceProtocol))
);
assert_eq!(
"governance-protocol".parse(),
Ok(ContentRef::Document(PlatformDoc::GovernanceProtocol))
);
assert_eq!(
"Constitution".parse(),
Ok(ContentRef::Document(PlatformDoc::Constitution))
);
assert!("proto".parse::<ContentRef>().is_err());
}
#[test]
fn content_ref_parses_prompt_slugs() {
let juror = PlatformDoc::Prompt("appeals_juror".parse().unwrap());
for written in [
"prompt:appeals_juror",
"Prompt:Appeals_Juror",
"prompt/appeals-juror",
"prompts/appeals_juror.md",
"prompts:appeals_juror",
] {
assert_eq!(
written.parse(),
Ok(ContentRef::Document(juror.clone())),
"{written}"
);
}
assert_eq!(juror.to_string(), "prompt:appeals_juror");
for index in ["prompts", "Prompts", "prompt"] {
assert_eq!(
index.parse(),
Ok(ContentRef::Document(PlatformDoc::Prompts)),
"{index}"
);
}
for rejected in [
"prompt:",
"prompt:2fast",
"prompt:tier 2",
"prompt:../secrets",
"prompt:tier2_reviewer/x",
"promptly",
] {
assert!(rejected.parse::<ContentRef>().is_err(), "{rejected}");
}
let long = format!("prompt:{}", "a".repeat(PromptName::MAX_LEN + 1));
assert!(long.parse::<ContentRef>().is_err());
}
#[test]
fn content_id_prefix_parses_eight_hex_digits_leniently() {
let p: ContentIdPrefix = "7ad26ccd".parse().unwrap();
assert_eq!(p.to_string(), "7ad26ccd");
assert_eq!(" 7AD26CCD\n".parse::<ContentIdPrefix>().unwrap(), p);
for rejected in ["7ad26cc", "7ad26ccd0", "7ad26ccg", "", "7ad2-6cc"] {
assert!(rejected.parse::<ContentIdPrefix>().is_err(), "{rejected}");
}
}
#[test]
fn content_id_prefix_bounds_cover_exactly_its_uuids() {
let id: Uuid = "7ad26ccd-922f-484a-a37c-51777344a98c".parse().unwrap();
let p = ContentIdPrefix::of(&id);
assert_eq!(p.to_string(), "7ad26ccd");
assert!(p.matches(&id));
let (lo, hi) = p.bounds();
assert_eq!(lo.to_string(), "7ad26ccd-0000-0000-0000-000000000000");
assert_eq!(hi.to_string(), "7ad26ccd-ffff-ffff-ffff-ffffffffffff");
assert!(lo <= id && id <= hi);
let next: Uuid =
"7ad26cce-0000-0000-0000-000000000000".parse().unwrap();
assert!(!p.matches(&next) && next > hi);
let top: ContentIdPrefix = "ffffffff".parse().unwrap();
assert_eq!(top.bounds().1, Uuid::max());
let bottom: ContentIdPrefix = "00000000".parse().unwrap();
assert_eq!(bottom.bounds().0, Uuid::nil());
}
#[test]
fn content_ref_reads_a_short_id_and_only_eight_digits() {
let r: ContentRef = "7ad26ccd".parse().unwrap();
assert_eq!(r, ContentRef::ContentPrefix("7ad26ccd".parse().unwrap()));
assert_eq!(r.kind_str(), "content");
assert_eq!(serde_json::to_string(&r).unwrap(), "\"7ad26ccd\"");
let full: ContentRef =
"7ad26ccd-922f-484a-a37c-51777344a98c".parse().unwrap();
assert!(matches!(full, ContentRef::Content(_)));
let err = "7ad26ccd-922f".parse::<ContentRef>().unwrap_err();
assert_eq!(err.leading_ref(), None);
}
#[test]
fn content_ref_pattern_admits_short_ids() {
let short = CONTENT_ID_PREFIX_PATTERN
.strip_prefix('^')
.and_then(|p| p.strip_suffix('$'))
.unwrap();
assert!(CONTENT_REF_PATTERN.contains(&format!("|{short}|")));
}
#[test]
fn document_patterns_agree() {
let docs = PLATFORM_DOC_PATTERN.strip_prefix("^(").unwrap();
assert!(CONTENT_REF_PATTERN.ends_with(&format!("|{docs}")));
let name = PROMPT_NAME_PATTERN
.strip_prefix('^')
.and_then(|p| p.strip_suffix('$'))
.unwrap();
assert!(PLATFORM_DOC_PATTERN.contains(&format!("|prompt:{name})$")));
}
#[test]
fn calendar_is_a_document_slug() {
assert_eq!(
"calendar".parse::<ContentRef>().unwrap(),
ContentRef::Document(PlatformDoc::Calendar)
);
assert_eq!(
"Calendar".parse::<PlatformDoc>().unwrap(),
PlatformDoc::Calendar
);
assert_eq!(PlatformDoc::Calendar.to_string(), "calendar");
assert_eq!(
"OBL-2026-0001".parse::<ContentRef>().unwrap(),
ContentRef::Governance("OBL-2026-0001".parse().unwrap())
);
assert_eq!(
"DATA-2026-0001".parse::<ContentRef>().unwrap(),
ContentRef::Governance("DATA-2026-0001".parse().unwrap())
);
assert_eq!(
"TEST-2026-0001".parse::<ContentRef>().unwrap(),
ContentRef::Governance("TEST-2026-0001".parse().unwrap())
);
}
#[test]
fn platform_doc_serde_uses_the_canonical_slug() {
let json =
serde_json::to_string(&PlatformDoc::GovernanceProtocol).unwrap();
assert_eq!(json, "\"protocol\"");
let doc: PlatformDoc =
serde_json::from_str("\"governance-protocol\"").unwrap();
assert_eq!(doc, PlatformDoc::GovernanceProtocol);
}
#[test]
fn ids_are_unique() {
let a = AgentId::new();
let b = AgentId::new();
assert_ne!(a, b);
}
#[test]
fn serde_round_trip() {
let id = PostId::new();
let json = serde_json::to_string(&id).unwrap();
let deserialized: PostId = serde_json::from_str(&json).unwrap();
assert_eq!(id, deserialized);
}
#[test]
fn display_shows_uuid() {
let id = CommunityId::new();
let display = id.to_string();
assert_eq!(display.len(), 36);
assert!(display.contains('-'));
}
#[test]
fn from_uuid_round_trip() {
let uuid = Uuid::new_v4();
let id = AgentId::from(uuid);
let back: Uuid = id.into();
assert_eq!(uuid, back);
}
#[test]
fn every_id_round_trips_through_its_own_display() {
let agent = AgentId::new();
assert_eq!(agent.to_string().parse::<AgentId>().unwrap(), agent);
let action = ModerationActionId::new();
assert_eq!(
action.to_string().parse::<ModerationActionId>().unwrap(),
action
);
let content = ContentId::new();
assert_eq!(content.to_string().parse::<ContentId>().unwrap(), content);
let feedback = FeedbackId::new();
assert_eq!(
feedback.to_string().parse::<FeedbackId>().unwrap(),
feedback
);
}
#[test]
fn parsing_a_non_uuid_is_an_error_not_a_panic() {
assert!("not-a-uuid".parse::<ContentId>().is_err());
assert!("".parse::<ContentId>().is_err());
}
#[test]
fn content_id_is_wire_compatible_with_a_bare_uuid() {
let uuid = Uuid::new_v4();
let typed = ContentId::from(uuid);
assert_eq!(
serde_json::to_string(&typed).unwrap(),
serde_json::to_string(&uuid).unwrap()
);
}
#[test]
fn every_moderation_target_narrows_losslessly() {
let uuid = Uuid::new_v4();
for (label, got) in [
("PostId", ModerationTargetId::from(PostId::from(uuid))),
("CommentId", ModerationTargetId::from(CommentId::from(uuid))),
("MessageId", ModerationTargetId::from(MessageId::from(uuid))),
("AgentId", ModerationTargetId::from(AgentId::from(uuid))),
("ContentId", ModerationTargetId::from(ContentId::from(uuid))),
] {
assert_eq!(
got.as_uuid(),
&uuid,
"{label} -> ModerationTargetId lost the uuid"
);
}
}
#[test]
fn resolved_ids_narrow_to_content_id_losslessly() {
let uuid = Uuid::new_v4();
assert_eq!(
ContentId::from(PostId::from(uuid)).as_uuid(),
&uuid,
"PostId -> ContentId lost the uuid"
);
assert_eq!(
ContentId::from(CommentId::from(uuid)).as_uuid(),
&uuid,
"CommentId -> ContentId lost the uuid"
);
assert_eq!(
ContentId::from(PostOrCommentId::Comment(CommentId::from(uuid)))
.as_uuid(),
&uuid,
"PostOrCommentId -> ContentId lost the uuid"
);
}
#[test]
fn json_is_plain_uuid_string() {
let uuid = Uuid::new_v4();
let id = AgentId::from(uuid);
let id_json = serde_json::to_string(&id).unwrap();
let uuid_json = serde_json::to_string(&uuid).unwrap();
assert_eq!(id_json, uuid_json);
}
#[cfg(feature = "schemars")]
#[test]
fn id_json_schema_is_inlined() {
use schemars::JsonSchema;
assert!(
<PostId as JsonSchema>::inline_schema(),
"PostId::inline_schema() must return true to avoid $ref in containing schemas"
);
assert!(<AgentId as JsonSchema>::inline_schema());
assert!(<CommentId as JsonSchema>::inline_schema());
assert!(<CommunityId as JsonSchema>::inline_schema());
assert!(<GovernanceLogId as JsonSchema>::inline_schema());
assert!(<ContentRef as JsonSchema>::inline_schema());
assert!(<FeedbackId as JsonSchema>::inline_schema());
assert!(<ContactRequestId as JsonSchema>::inline_schema());
#[derive(schemars::JsonSchema)]
#[allow(dead_code)]
struct Container {
post_id: PostId,
agent_id: Option<AgentId>,
gov_id: GovernanceLogId,
maybe_gov_id: Option<GovernanceLogId>,
content_ref: ContentRef,
maybe_content_ref: Option<ContentRef>,
feedback_id: FeedbackId,
}
let schema = schemars::schema_for!(Container);
let value = serde_json::to_value(&schema).unwrap();
assert!(
value.get("$defs").is_none(),
"no $defs should be emitted for ID-only container; got schema: {value}"
);
let post_id = &value["properties"]["post_id"];
assert!(
post_id.get("$ref").is_none(),
"post_id must not be a $ref; got: {post_id}"
);
assert_eq!(post_id["type"], "string");
assert_eq!(post_id["format"], "uuid");
let agent_id = &value["properties"]["agent_id"];
assert!(
agent_id.get("$ref").is_none(),
"agent_id must not be a $ref; got: {agent_id}"
);
let agent_id_str = agent_id.to_string();
assert!(
!agent_id_str.contains("$ref"),
"agent_id schema must contain no $ref anywhere; got: {agent_id}"
);
assert!(
agent_id_str.contains("\"format\":\"uuid\""),
"agent_id should still carry format=uuid; got: {agent_id}"
);
for field in
["gov_id", "maybe_gov_id", "content_ref", "maybe_content_ref"]
{
let f = &value["properties"][field];
assert!(
!f.to_string().contains("$ref"),
"{field} must contain no $ref anywhere; got: {f}"
);
}
assert_eq!(value["properties"]["gov_id"]["type"], "string");
assert_eq!(
value["properties"]["gov_id"]["pattern"],
GOVERNANCE_LOG_ID_PATTERN
);
assert!(
value["properties"]["maybe_gov_id"]
.to_string()
.contains("GOV|APP"),
"Option<GovernanceLogId> should keep the citation pattern; got: {}",
value["properties"]["maybe_gov_id"]
);
assert_eq!(value["properties"]["content_ref"]["type"], "string");
}
#[test]
fn governance_log_id_accepts_only_citation_shapes() {
for (good, prefix) in [
("GOV-2026-0006", GovernanceLogPrefix::Gov),
("APP-2026-0003", GovernanceLogPrefix::App),
("AMD-2026-0001", GovernanceLogPrefix::Amd),
("KEY-2026-0001", GovernanceLogPrefix::Key),
("REC-2026-0001", GovernanceLogPrefix::Rec),
("OBL-2026-0001", GovernanceLogPrefix::Obl),
("DATA-2026-0001", GovernanceLogPrefix::Data),
("TEST-2026-0001", GovernanceLogPrefix::Test),
("GOV-1999-0000", GovernanceLogPrefix::Gov),
] {
let id = good.parse::<GovernanceLogId>().unwrap();
assert_eq!(id.as_str(), good, "{good} should parse");
assert_eq!(id.prefix(), prefix);
let letters = good.split('-').next().unwrap();
assert_eq!(id.prefix().as_str(), letters);
assert_eq!(id.prefix().to_string().parse(), Ok(prefix));
}
let newer: GovernanceLogId =
serde_json::from_value(serde_json::json!("ZZZ-2026-0001")).unwrap();
assert_eq!(newer.prefix(), GovernanceLogPrefix::Unknown);
assert_eq!(newer.as_str(), "ZZZ-2026-0001");
assert!("ZZZ-2026-0001".parse::<GovernanceLogId>().is_err());
assert!(!GovernanceLogId::is_citation_shaped("ZZZ-2026-0001"));
for junk in [
"zzz-2026-0001",
"ZZZZZ-2026-0001",
"ZZ-2026-0001",
"ZZZ-2026-1",
] {
assert!(
serde_json::from_value::<GovernanceLogId>(serde_json::json!(
junk
))
.is_err(),
"{junk}"
);
}
assert_eq!(
GovernanceLogId::from_parts(GovernanceLogPrefix::Unknown, 2026, 1),
None
);
for prefix in GovernanceLogPrefix::ALL {
let want = if matches!(
prefix,
GovernanceLogPrefix::Data | GovernanceLogPrefix::Test
) {
4
} else {
3
};
assert_eq!(prefix.as_str().len(), want, "{prefix}");
}
let series = GovernanceLogPrefix::ALL.map(|p| p.as_str()).join("|");
assert!(
GOVERNANCE_LOG_ID_PATTERN.contains(&format!("({series})")),
"{GOVERNANCE_LOG_ID_PATTERN}"
);
assert!(
CONTENT_REF_PATTERN.contains(&format!("({series})")),
"{CONTENT_REF_PATTERN}"
);
assert_eq!(
"GOV-2026-006".parse::<GovernanceLogId>().unwrap().as_str(),
"GOV-2026-0006"
);
assert_eq!(
"gov-2026-0006".parse::<GovernanceLogId>().unwrap().as_str(),
"GOV-2026-0006"
);
for bad in [
"",
"GOV-26-0006",
"MOD-2026-0006",
"GOV-2026-0006-1",
"GOV-202X-0006",
"DAT-2026-0001",
"DATAS-2026-0001",
"DATA-2026-00001",
"DATA-2026",
"TES-2026-0001",
"TESTS-2026-0001",
"TEST-2026-00001",
"3f1a0000-0000-0000-0000-000000000000",
] {
assert!(
bad.parse::<GovernanceLogId>().is_err(),
"{bad:?} should not parse as a GovernanceLogId"
);
}
}
#[test]
fn governance_log_id_is_wire_compatible_with_a_bare_string() {
let id: GovernanceLogId = "GOV-2026-0006".parse().unwrap();
assert_eq!(serde_json::to_string(&id).unwrap(), "\"GOV-2026-0006\"");
let back: GovernanceLogId =
serde_json::from_str("\"GOV-2026-0006\"").unwrap();
assert_eq!(back, id);
assert!(serde_json::from_str::<GovernanceLogId>("\"nope\"").is_err());
}
#[test]
fn content_ref_round_trips_as_a_bare_string() {
let uuid = Uuid::new_v4();
let content = ContentRef::from(ContentId::from(uuid));
assert_eq!(
serde_json::to_value(&content).unwrap(),
serde_json::json!(uuid.to_string())
);
assert_eq!(
serde_json::from_value::<ContentRef>(serde_json::json!(
uuid.to_string()
))
.unwrap(),
content
);
let gov = ContentRef::Governance("APP-2026-0003".parse().unwrap());
assert_eq!(
serde_json::to_value(&gov).unwrap(),
serde_json::json!("APP-2026-0003")
);
assert_eq!(
serde_json::from_value::<ContentRef>(serde_json::json!(
"APP-2026-0003"
))
.unwrap(),
gov
);
assert!(gov.is_governance());
assert!(!content.is_governance());
assert_eq!(gov.kind_str(), "governance");
assert_eq!(content.kind_str(), "content");
assert_eq!(content.as_content(), Some(ContentId::from(uuid)));
assert!(content.as_governance().is_none());
assert!("not-an-id".parse::<ContentRef>().is_err());
assert!(
serde_json::from_value::<ContentRef>(serde_json::json!(
"not-an-id"
))
.is_err()
);
}
#[test]
fn every_readable_id_narrows_to_a_content_ref() {
let uuid = Uuid::new_v4();
for (label, got) in [
("PostId", ContentRef::from(PostId::from(uuid))),
("CommentId", ContentRef::from(CommentId::from(uuid))),
("ContentId", ContentRef::from(ContentId::from(uuid))),
] {
assert_eq!(
got,
ContentRef::Content(ContentId::from(uuid)),
"{label} -> ContentRef lost the uuid"
);
}
let gov: GovernanceLogId = "GOV-2026-0006".parse().unwrap();
assert_eq!(ContentRef::from(gov.clone()), ContentRef::Governance(gov));
}
#[test]
fn string_shaped_ids_round_trip_through_display() {
let gov: GovernanceLogId = "GOV-2026-0006".parse().unwrap();
assert_eq!(gov.to_string().parse::<GovernanceLogId>().unwrap(), gov);
let r = ContentRef::Governance(gov);
assert_eq!(r.to_string().parse::<ContentRef>().unwrap(), r);
let r = ContentRef::Content(ContentId::new());
assert_eq!(r.to_string().parse::<ContentRef>().unwrap(), r);
}
#[test]
fn post_or_comment_post_variant() {
let inner = PostId::new();
let tagged = PostOrCommentId::Post(inner);
assert!(tagged.is_post());
assert!(!tagged.is_comment());
assert_eq!(tagged.as_post(), Some(inner));
assert_eq!(tagged.as_comment(), None);
assert_eq!(tagged.as_uuid(), *inner.as_uuid());
assert_eq!(tagged.kind_str(), "post");
}
#[test]
fn post_or_comment_comment_variant() {
let inner = CommentId::new();
let tagged = PostOrCommentId::Comment(inner);
assert!(tagged.is_comment());
assert!(!tagged.is_post());
assert_eq!(tagged.as_comment(), Some(inner));
assert_eq!(tagged.as_post(), None);
assert_eq!(tagged.as_uuid(), *inner.as_uuid());
assert_eq!(tagged.kind_str(), "comment");
}
#[test]
fn post_or_comment_from_conversions() {
let post = PostId::new();
let comment = CommentId::new();
let via_post: PostOrCommentId = post.into();
let via_comment: PostOrCommentId = comment.into();
assert_eq!(via_post, PostOrCommentId::Post(post));
assert_eq!(via_comment, PostOrCommentId::Comment(comment));
}
#[test]
fn post_or_comment_display_is_kind_colon_uuid() {
let post = PostId::new();
let tagged = PostOrCommentId::Post(post);
let rendered = tagged.to_string();
assert!(rendered.starts_with("post:"));
assert!(rendered.contains(&post.to_string()));
}
const FULL: &str = "7ad26ccd-922f-484a-a37c-51777344a98c";
#[test]
fn a_bad_id_fails_in_plain_words() {
let bad = "7ad26ccd-922f-484a-a37c-51777344a";
let want = format!(
"not a valid PostId: expected a UUID like \"{FULL}\", got \"{bad}\""
);
assert_eq!(bad.parse::<PostId>().unwrap_err().to_string(), want);
let err = serde_json::from_str::<PostId>(&format!("\"{bad}\""))
.unwrap_err()
.to_string();
assert!(err.starts_with(&want), "{err}");
assert!(!err.contains("group"), "{err}");
let err = serde_json::from_str::<AgentId>("5")
.unwrap_err()
.to_string();
assert!(err.contains("expected AgentId as a UUID string"), "{err}");
let long = "x".repeat(500);
let msg = long.parse::<CommentId>().unwrap_err().to_string();
assert!(msg.len() < 200 && msg.ends_with("…\""), "{msg}");
}
#[test]
fn ids_still_round_trip() {
let id: PostId = FULL.parse().unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(serde_json::from_str::<PostId>(&json).unwrap(), id);
let map: std::collections::HashMap<PostId, i64> =
[(id, 3)].into_iter().collect();
let json = serde_json::to_string(&map).unwrap();
let back: std::collections::HashMap<PostId, i64> =
serde_json::from_str(&json).unwrap();
assert_eq!(back, map);
}
fn target(s: &str) -> Result<ContentTarget, ContentTargetError> {
s.parse()
}
#[test]
fn full_and_short_targets_parse() {
let full = target(FULL).unwrap();
assert_eq!(full.full().unwrap().to_string(), FULL);
assert_eq!(full.to_string(), FULL);
let short = target(" 7AD26CCD ").unwrap();
assert!(short.full().is_none());
assert_eq!(short.to_string(), "7ad26ccd");
for variant in [
FULL.replace('-', ""),
FULL.to_uppercase(),
FULL.replace('-', "\u{2011}"),
] {
assert_eq!(target(&variant).unwrap(), full, "{variant}");
}
}
#[test]
fn a_truncated_uuid_is_an_error_not_its_prefix() {
let err = target("7ad26ccd-922f-484a-a37c-51777344a").unwrap_err();
let msg = err.in_field("reply_to").to_string();
assert!(
msg.starts_with(
"`reply_to` must be a post or comment id: a full UUID or its first 8 hex digits"
),
"{msg}"
);
for internal in [
"group",
"UUID parsing",
"invalid length",
"invalid character",
] {
assert!(!msg.contains(internal), "{internal} leaked: {msg}");
}
assert!(target("7ad26cc").is_err());
assert!(target("7ad26ccd9").is_err());
}
#[test]
fn trailing_text_names_the_id_it_starts_with() {
let msg = target(&format!("{FULL} (the reply above)"))
.unwrap_err()
.to_string();
assert!(msg.contains(&format!("starts with the id {FULL}")), "{msg}");
}
#[derive(Debug, serde::Deserialize)]
struct Probe {
#[serde(deserialize_with = "content_target::reply_to")]
#[allow(dead_code)]
reply_to: ContentTarget,
}
#[test]
fn target_errors_name_the_field_for_strings_and_other_types() {
let err = serde_json::from_str::<Probe>(r#"{"reply_to": "nope"}"#)
.unwrap_err();
assert!(
err.to_string()
.starts_with("`reply_to` must be a post or comment id"),
"{err}"
);
let err =
serde_json::from_str::<Probe>(r#"{"reply_to": 5}"#).unwrap_err();
assert!(
err.to_string()
.contains("expected `reply_to` to be a post or comment id"),
"{err}"
);
}
#[cfg(feature = "schemars")]
#[test]
fn target_schema_is_inline_and_admits_both_forms() {
let schema = schemars::schema_for!(ContentTarget);
let v = schema.as_value();
assert!(v.get("$ref").is_none() && v.get("$defs").is_none(), "{v}");
assert!(v.get("format").is_none(), "a short id is not format: uuid");
assert_eq!(v["pattern"], CONTENT_TARGET_PATTERN);
}
}