use serde::Serialize;
use serde_json::Value;
use super::{CHAT_SCHEMA, DB_SCHEMA, SETTINGS_SCHEMA};
use crate::entities::attachment::AttachMode;
use crate::entities::chat::DeletedCause;
use crate::entities::chat_file::FileOrigin;
use crate::entities::message::{MessageFinish, MessageRole};
use crate::entities::sampling::{ReasoningEffort, Verbosity};
use crate::entities::self_model::GoalStatus;
use crate::entities::subagent::{RunKind, RunOutcome};
use crate::shared::config::{
AutoTitleMode, CodeBlockBackground, FlashAttn, ImpersonationMode, MediaResolution, NoteOrder,
PythonMode, ServerMode, SpecType, Theme, ThemeMode, TtsMode, VideoProvider, WebProvider,
};
use crate::shared::embed_prefix::EmbedConvention;
use crate::shared::osc52::Osc52Mode;
#[derive(Debug, Clone, Copy)]
pub(super) enum Held {
Settings,
Chats,
Sampling,
SelfModels,
}
impl Held {
fn newest(self) -> u32 {
match self {
Held::Settings => SETTINGS_SCHEMA,
Held::Chats => CHAT_SCHEMA,
Held::Sampling => SETTINGS_SCHEMA.min(CHAT_SCHEMA),
Held::SelfModels => DB_SCHEMA,
}
}
}
pub(super) struct Stored {
pub(super) variant: &'static str,
pub(super) pinned: &'static str,
pub(super) wire: Value,
pub(super) since: u32,
}
pub(super) struct Ledger {
pub(super) name: &'static str,
pub(super) held: Held,
pub(super) values: Vec<Stored>,
}
fn wire<T: Serialize>(value: &T) -> Value {
serde_json::to_value(value).expect("a unit variant serializes")
}
macro_rules! ledger {
($ty:ident in $held:ident {
$($since:literal => [$($variant:ident = $pinned:literal),+ $(,)?]),+ $(,)?
}) => {{
fn since(value: $ty) -> u32 {
match value {
$($($ty::$variant)|+ => $since,)+
}
}
Ledger {
name: stringify!($ty),
held: Held::$held,
values: vec![$($(Stored {
variant: stringify!($variant),
pinned: $pinned,
wire: wire(&$ty::$variant),
since: since($ty::$variant),
},)+)+],
}
}};
}
pub(super) fn ledgers() -> Vec<Ledger> {
vec![
ledger!(ServerMode in Settings {
1 => [
Managed = "managed", External = "external", OpenAi = "openai",
Gemini = "gemini", Claude = "claude", Grok = "grok",
],
4 => [OpenRouter = "openrouter"],
7 => [DeepSeek = "deepseek"],
}),
ledger!(ImpersonationMode in Settings {
1 => [
Shared = "shared", Managed = "managed", External = "external",
OpenAi = "openai", Gemini = "gemini", Claude = "claude", Grok = "grok",
],
4 => [OpenRouter = "openrouter"],
7 => [DeepSeek = "deepseek"],
}),
ledger!(TtsMode in Settings {
1 => [OpenAi = "openai", Gemini = "gemini", External = "external"],
4 => [OpenRouter = "openrouter"],
}),
ledger!(VideoProvider in Settings {
4 => [Gemini = "gemini", OpenRouter = "openrouter"],
}),
ledger!(MediaResolution in Settings { 1 => [Low = "low", Medium = "medium"] }),
ledger!(EmbedConvention in Settings {
1 => [None = "none", E5 = "e5", E5Instruct = "e5-instruct"],
}),
ledger!(FlashAttn in Settings { 1 => [Auto = "auto", On = "on", Off = "off"] }),
ledger!(SpecType in Settings {
1 => [
None = "none", DraftSimple = "draft-simple", DraftEagle3 = "draft-eagle3",
DraftMtp = "draft-mtp", NgramSimple = "ngram-simple",
NgramMapK = "ngram-map-k", NgramMapK4v = "ngram-map-k4v",
NgramMod = "ngram-mod", NgramCache = "ngram-cache",
],
}),
ledger!(WebProvider in Settings { 2 => [Auto = "auto", FreeOnly = "freeonly"] }),
ledger!(PythonMode in Settings { 1 => [Wasmer = "wasmer", Local = "local"] }),
ledger!(Theme in Settings { 1 => [Auto = "auto", Dark = "dark", Light = "light"] }),
ledger!(ThemeMode in Settings { 3 => [System = "system", Full = "full", Mono = "mono"] }),
ledger!(CodeBlockBackground in Settings { 5 => [Inverted = "inverted", Normal = "normal"] }),
ledger!(AutoTitleMode in Settings {
1 => [
AfterUserMessage = "after_user_message",
AfterAssistantReply = "after_assistant_reply",
Off = "off",
],
}),
ledger!(NoteOrder in Settings {
2 => [NewestFirst = "newest_first", OldestFirst = "oldest_first"],
}),
ledger!(Osc52Mode in Settings { 1 => [Auto = "auto", Always = "always", Off = "off"] }),
ledger!(ReasoningEffort in Sampling {
1 => [
None = "none", Minimal = "minimal", Low = "low", Medium = "medium",
High = "high", XHigh = "xhigh",
],
5 => [Max = "max"],
}),
ledger!(Verbosity in Sampling { 1 => [Low = "low", Medium = "medium", High = "high"] }),
ledger!(ServerMode in Chats {
1 => [
Managed = "managed", External = "external", OpenAi = "openai",
Gemini = "gemini", Claude = "claude", Grok = "grok",
],
4 => [OpenRouter = "openrouter"],
6 => [DeepSeek = "deepseek"],
}),
ledger!(MessageRole in Chats {
1 => [System = "system", User = "user", Assistant = "assistant", Tool = "tool"],
}),
ledger!(MessageFinish in Chats {
4 => [Stop = "stop", Length = "length", Cancelled = "cancelled", Error = "error"],
}),
ledger!(AttachMode in Chats { 1 => [Inline = "inline", ByReference = "by_reference"] }),
ledger!(FileOrigin in Chats {
4 => [Sandbox = "sandbox", Recovered = "recovered", Attached = "attached"],
}),
ledger!(DeletedCause in Chats {
1 => [
DeleteExchange = "DeleteExchange", Regenerate = "Regenerate",
Rewrite = "Rewrite",
],
}),
ledger!(RunKind in Chats { 2 => [Subagent = "subagent"], 3 => [Dialogue = "dialogue"] }),
ledger!(RunOutcome in Chats {
2 => [
Completed = "completed", Cancelled = "cancelled", TimedOut = "timed_out",
Failed = "failed", RoundLimit = "round_limit",
],
}),
ledger!(GoalStatus in SelfModels {
1 => [Active = "Active", Completed = "Completed", Abandoned = "Abandoned"],
}),
]
}
pub(super) fn efforts() -> Vec<(ReasoningEffort, u32)> {
let ledger = ledgers()
.into_iter()
.find(|l| l.name == "ReasoningEffort")
.expect("the effort scale has a ledger");
ledger
.values
.into_iter()
.map(|v| (serde_json::from_value(v.wire).unwrap(), v.since))
.collect()
}
#[test]
fn every_stored_value_arrived_in_a_schema_its_files_have() {
for ledger in ledgers() {
let newest = ledger.held.newest();
for v in &ledger.values {
assert!(
(1..=newest).contains(&v.since),
"{}::{} names schema {}, and {:?} files are at {newest}: write the step",
ledger.name,
v.variant,
v.since,
ledger.held
);
}
}
}
#[test]
fn a_stored_value_keeps_its_wire_form() {
for ledger in ledgers() {
for v in &ledger.values {
assert_eq!(
v.wire,
Value::from(v.pinned),
"{}::{} is written differently: a renamed stored value is a breaking change",
ledger.name,
v.variant
);
}
}
}