use rand::seq::IndexedRandom;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplyArchetype {
AgreeAndExpand,
RespectfulDisagree,
AddData,
AskQuestion,
ShareExperience,
}
impl ReplyArchetype {
pub fn select(rng: &mut impl rand::Rng) -> Self {
let choices: &[(Self, u32)] = &[
(Self::AgreeAndExpand, 30),
(Self::AskQuestion, 25),
(Self::ShareExperience, 20),
(Self::AddData, 15),
(Self::RespectfulDisagree, 10),
];
let total: u32 = choices.iter().map(|(_, w)| w).sum();
let mut roll = rng.random_range(0..total);
for (archetype, weight) in choices {
if roll < *weight {
return *archetype;
}
roll -= weight;
}
Self::AgreeAndExpand
}
pub fn prompt_fragment(self) -> &'static str {
match self {
Self::AgreeAndExpand => {
"Approach: Agree with the author's point and extend it with \
an additional insight or implication they didn't mention."
}
Self::RespectfulDisagree => {
"Approach: Respectfully offer an alternative take. Start with \
what you agree with, then pivot to where you see it differently. \
Keep it constructive — never confrontational."
}
Self::AddData => {
"Approach: Add a concrete data point, stat, example, or case study \
that supports or contextualizes the topic. Cite specifics when possible."
}
Self::AskQuestion => {
"Approach: Ask a thoughtful follow-up question that shows you've engaged \
deeply with the tweet. The question should invite the author to elaborate."
}
Self::ShareExperience => {
"Approach: Share a brief personal experience or observation related to the \
topic. Use 'I' language and keep it genuine and specific."
}
}
}
}
impl std::fmt::Display for ReplyArchetype {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AgreeAndExpand => write!(f, "agree_and_expand"),
Self::RespectfulDisagree => write!(f, "respectful_disagree"),
Self::AddData => write!(f, "add_data"),
Self::AskQuestion => write!(f, "ask_question"),
Self::ShareExperience => write!(f, "share_experience"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TweetFormat {
List,
ContrarianTake,
MostPeopleThinkX,
Storytelling,
BeforeAfter,
Question,
Tip,
}
impl TweetFormat {
const ALL: &'static [Self] = &[
Self::List,
Self::ContrarianTake,
Self::MostPeopleThinkX,
Self::Storytelling,
Self::BeforeAfter,
Self::Question,
Self::Tip,
];
pub fn select(recent: &[Self], rng: &mut impl rand::Rng) -> Self {
let available: Vec<Self> = Self::ALL
.iter()
.copied()
.filter(|f| !recent.contains(f))
.collect();
if available.is_empty() {
*Self::ALL.choose(rng).expect("ALL is non-empty")
} else {
*available.choose(rng).expect("available is non-empty")
}
}
pub fn prompt_fragment(self) -> &'static str {
match self {
Self::List => {
"Format: Write a numbered list of 3-5 quick tips or insights. \
Keep each item to one line."
}
Self::ContrarianTake => {
"Format: Start with a common belief, then challenge it with an \
unexpected truth. Structure: 'Everyone says X. But actually, Y.'"
}
Self::MostPeopleThinkX => {
"Format: 'Most people think [common assumption]. The reality: [insight].'"
}
Self::Storytelling => {
"Format: Tell a very brief story or anecdote (2-3 sentences) that \
illustrates the topic. End with the lesson."
}
Self::BeforeAfter => {
"Format: Show a transformation. 'Before: [old way]. After: [new way]. \
[Brief insight on why the change matters].'"
}
Self::Question => {
"Format: Pose a thought-provoking question to the audience that invites \
engagement. Optionally share your own answer in 1-2 sentences."
}
Self::Tip => {
"Format: Share one specific, actionable tip. Be concrete — include the \
exact steps or command, not vague advice."
}
}
}
}
impl std::fmt::Display for TweetFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::List => write!(f, "list"),
Self::ContrarianTake => write!(f, "contrarian_take"),
Self::MostPeopleThinkX => write!(f, "most_people_think_x"),
Self::Storytelling => write!(f, "storytelling"),
Self::BeforeAfter => write!(f, "before_after"),
Self::Question => write!(f, "question"),
Self::Tip => write!(f, "tip"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadStructure {
Transformation,
Framework,
Mistakes,
Analysis,
}
impl ThreadStructure {
const ALL: &'static [Self] = &[
Self::Transformation,
Self::Framework,
Self::Mistakes,
Self::Analysis,
];
pub fn select(rng: &mut impl rand::Rng) -> Self {
*Self::ALL.choose(rng).expect("ALL is non-empty")
}
pub fn prompt_fragment(self) -> &'static str {
match self {
Self::Transformation => {
"Structure: Tell a transformation story. Start with the 'before' state, \
walk through the key turning points, and end with the 'after' state \
and lessons learned."
}
Self::Framework => {
"Structure: Present a step-by-step framework. Tweet 1 hooks with the \
problem, subsequent tweets present each step, and the last tweet \
summarizes the framework."
}
Self::Mistakes => {
"Structure: Share mistakes and lessons. Tweet 1 hooks with 'N mistakes \
I made doing X', each subsequent tweet is one mistake with what you \
learned, and the last tweet is the key takeaway."
}
Self::Analysis => {
"Structure: Deep-dive analysis. Tweet 1 states the thesis, subsequent \
tweets provide evidence or arguments, and the last tweet draws a conclusion."
}
}
}
}
impl std::fmt::Display for ThreadStructure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Transformation => write!(f, "transformation"),
Self::Framework => write!(f, "framework"),
Self::Mistakes => write!(f, "mistakes"),
Self::Analysis => write!(f, "analysis"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reply_archetype_select_returns_valid() {
let mut rng = rand::rng();
for _ in 0..100 {
let _ = ReplyArchetype::select(&mut rng);
}
}
#[test]
fn reply_archetype_select_distribution() {
let mut rng = rand::rng();
let mut counts = [0u32; 5];
for _ in 0..1000 {
let archetype = ReplyArchetype::select(&mut rng);
match archetype {
ReplyArchetype::AgreeAndExpand => counts[0] += 1,
ReplyArchetype::RespectfulDisagree => counts[1] += 1,
ReplyArchetype::AddData => counts[2] += 1,
ReplyArchetype::AskQuestion => counts[3] += 1,
ReplyArchetype::ShareExperience => counts[4] += 1,
}
}
for (i, count) in counts.iter().enumerate() {
assert!(
*count > 0,
"archetype index {i} never selected in 1000 samples"
);
}
assert!(
counts[0] > counts[1],
"AgreeAndExpand should be more frequent"
);
}
#[test]
fn reply_archetype_prompt_fragments_non_empty() {
let archetypes = [
ReplyArchetype::AgreeAndExpand,
ReplyArchetype::RespectfulDisagree,
ReplyArchetype::AddData,
ReplyArchetype::AskQuestion,
ReplyArchetype::ShareExperience,
];
for a in archetypes {
assert!(!a.prompt_fragment().is_empty());
}
}
#[test]
fn reply_archetype_display() {
assert_eq!(
ReplyArchetype::AgreeAndExpand.to_string(),
"agree_and_expand"
);
assert_eq!(ReplyArchetype::AskQuestion.to_string(), "ask_question");
}
#[test]
fn tweet_format_select_avoids_recent() {
let mut rng = rand::rng();
let recent = vec![TweetFormat::List, TweetFormat::Tip, TweetFormat::Question];
for _ in 0..50 {
let format = TweetFormat::select(&recent, &mut rng);
assert!(!recent.contains(&format));
}
}
#[test]
fn tweet_format_select_clears_when_all_recent() {
let mut rng = rand::rng();
let recent: Vec<TweetFormat> = TweetFormat::ALL.to_vec();
let format = TweetFormat::select(&recent, &mut rng);
assert!(TweetFormat::ALL.contains(&format));
}
#[test]
fn tweet_format_prompt_fragments_non_empty() {
for f in TweetFormat::ALL {
assert!(!f.prompt_fragment().is_empty());
}
}
#[test]
fn tweet_format_display() {
assert_eq!(TweetFormat::List.to_string(), "list");
assert_eq!(TweetFormat::ContrarianTake.to_string(), "contrarian_take");
assert_eq!(TweetFormat::BeforeAfter.to_string(), "before_after");
}
#[test]
fn thread_structure_select_returns_valid() {
let mut rng = rand::rng();
for _ in 0..50 {
let structure = ThreadStructure::select(&mut rng);
assert!(ThreadStructure::ALL.contains(&structure));
}
}
#[test]
fn thread_structure_prompt_fragments_non_empty() {
for s in ThreadStructure::ALL {
assert!(!s.prompt_fragment().is_empty());
}
}
#[test]
fn thread_structure_display() {
assert_eq!(
ThreadStructure::Transformation.to_string(),
"transformation"
);
assert_eq!(ThreadStructure::Framework.to_string(), "framework");
assert_eq!(ThreadStructure::Mistakes.to_string(), "mistakes");
assert_eq!(ThreadStructure::Analysis.to_string(), "analysis");
}
#[test]
fn reply_archetype_all_variants_reachable() {
use std::collections::HashSet;
let mut rng = rand::rng();
let mut seen = HashSet::new();
for _ in 0..10_000 {
seen.insert(ReplyArchetype::select(&mut rng).to_string());
}
assert_eq!(
seen.len(),
5,
"expected all 5 reply archetypes, got {seen:?}"
);
}
#[test]
fn tweet_format_all_variants_reachable() {
use std::collections::HashSet;
let mut rng = rand::rng();
let mut seen = HashSet::new();
let recent: Vec<TweetFormat> = vec![];
for _ in 0..10_000 {
seen.insert(TweetFormat::select(&recent, &mut rng).to_string());
}
assert_eq!(seen.len(), 7, "expected all 7 tweet formats, got {seen:?}");
}
#[test]
fn thread_structure_all_variants_reachable() {
use std::collections::HashSet;
let mut rng = rand::rng();
let mut seen = HashSet::new();
for _ in 0..10_000 {
seen.insert(ThreadStructure::select(&mut rng).to_string());
}
assert_eq!(
seen.len(),
4,
"expected all 4 thread structures, got {seen:?}"
);
}
#[test]
fn tweet_format_display_all_variants() {
assert_eq!(TweetFormat::List.to_string(), "list");
assert_eq!(TweetFormat::ContrarianTake.to_string(), "contrarian_take");
assert_eq!(
TweetFormat::MostPeopleThinkX.to_string(),
"most_people_think_x"
);
assert_eq!(TweetFormat::Storytelling.to_string(), "storytelling");
assert_eq!(TweetFormat::BeforeAfter.to_string(), "before_after");
assert_eq!(TweetFormat::Question.to_string(), "question");
assert_eq!(TweetFormat::Tip.to_string(), "tip");
}
#[test]
fn reply_archetype_display_all_variants() {
assert_eq!(
ReplyArchetype::AgreeAndExpand.to_string(),
"agree_and_expand"
);
assert_eq!(
ReplyArchetype::RespectfulDisagree.to_string(),
"respectful_disagree"
);
assert_eq!(ReplyArchetype::AddData.to_string(), "add_data");
assert_eq!(ReplyArchetype::AskQuestion.to_string(), "ask_question");
assert_eq!(
ReplyArchetype::ShareExperience.to_string(),
"share_experience"
);
}
#[test]
fn tweet_format_select_single_available() {
let mut rng = rand::rng();
let recent = vec![
TweetFormat::List,
TweetFormat::ContrarianTake,
TweetFormat::MostPeopleThinkX,
TweetFormat::BeforeAfter,
TweetFormat::Question,
TweetFormat::Tip,
];
for _ in 0..50 {
let picked = TweetFormat::select(&recent, &mut rng);
assert_eq!(
picked,
TweetFormat::Storytelling,
"only Storytelling should be available"
);
}
}
#[test]
fn reply_archetype_prompt_fragment_content() {
let frag = ReplyArchetype::AgreeAndExpand.prompt_fragment();
assert!(frag.contains("Agree"));
let frag = ReplyArchetype::RespectfulDisagree.prompt_fragment();
assert!(frag.contains("alternative"));
let frag = ReplyArchetype::AddData.prompt_fragment();
assert!(frag.contains("data"));
let frag = ReplyArchetype::AskQuestion.prompt_fragment();
assert!(frag.contains("question"));
let frag = ReplyArchetype::ShareExperience.prompt_fragment();
assert!(frag.contains("experience"));
}
#[test]
fn tweet_format_prompt_fragment_content() {
let frag = TweetFormat::List.prompt_fragment();
assert!(frag.contains("list"));
let frag = TweetFormat::ContrarianTake.prompt_fragment();
assert!(frag.contains("challenge"));
let frag = TweetFormat::Storytelling.prompt_fragment();
assert!(frag.contains("story"));
let frag = TweetFormat::BeforeAfter.prompt_fragment();
assert!(frag.contains("Before"));
let frag = TweetFormat::Question.prompt_fragment();
assert!(frag.contains("question"));
let frag = TweetFormat::Tip.prompt_fragment();
assert!(frag.contains("tip"));
}
#[test]
fn thread_structure_prompt_fragment_content() {
let frag = ThreadStructure::Transformation.prompt_fragment();
assert!(frag.contains("transformation"));
let frag = ThreadStructure::Framework.prompt_fragment();
assert!(frag.contains("framework"));
let frag = ThreadStructure::Mistakes.prompt_fragment();
assert!(frag.contains("mistakes"));
let frag = ThreadStructure::Analysis.prompt_fragment();
assert!(frag.contains("analysis"));
}
#[test]
fn tweet_format_all_count() {
assert_eq!(TweetFormat::ALL.len(), 7);
}
#[test]
fn thread_structure_all_count() {
assert_eq!(ThreadStructure::ALL.len(), 4);
}
#[test]
fn reply_archetype_equality() {
assert_eq!(ReplyArchetype::AddData, ReplyArchetype::AddData);
assert_ne!(ReplyArchetype::AddData, ReplyArchetype::AskQuestion);
}
#[test]
fn tweet_format_equality() {
assert_eq!(TweetFormat::Tip, TweetFormat::Tip);
assert_ne!(TweetFormat::Tip, TweetFormat::List);
}
#[test]
fn thread_structure_equality() {
assert_eq!(ThreadStructure::Analysis, ThreadStructure::Analysis);
assert_ne!(ThreadStructure::Analysis, ThreadStructure::Framework);
}
#[test]
fn tweet_format_empty_recent() {
let mut rng = rand::rng();
let format = TweetFormat::select(&[], &mut rng);
assert!(TweetFormat::ALL.contains(&format));
}
#[test]
fn thread_structure_debug() {
let debug = format!("{:?}", ThreadStructure::Transformation);
assert!(debug.contains("Transformation"));
}
#[test]
fn tweet_format_debug() {
let debug = format!("{:?}", TweetFormat::List);
assert!(debug.contains("List"));
}
#[test]
fn reply_archetype_debug() {
let debug = format!("{:?}", ReplyArchetype::AddData);
assert!(debug.contains("AddData"));
}
#[test]
fn tweet_format_most_people_think_x_prompt() {
let frag = TweetFormat::MostPeopleThinkX.prompt_fragment();
assert!(frag.contains("Most people"));
}
}