1//! The archive: every request lmjtfy has had answered, and the questions 2//! people asked. These are the messages the Worker and the archive's Durable 3//! Object exchange. No I/O and no clock. 4//! 5//! The rule (the user, 2026-10-02): the same request is never sent to the 6//! same model twice. A request is the exact body, and it is kept with its 7//! response under the model it went to. Asking again returns that response. 8//! The archive is also what makes the call, so two visitors asking the same 9//! new thing at once share one request. 10#![forbid(unsafe_code)] 11 12use ask::{Sent, Wanted}; 13use serde::{Deserialize, Serialize}; 14 15pub mod event; 16pub use event::{Event, Origin, Seen as Report}; 17 18/// What the Worker asks the archive. 19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 20pub enum Ask { 21 /// Jev's answer to `wanted` about `input`. `request` is the body the 22 /// Worker prepared and shows on the page; the archive prepares it again 23 /// from `wanted`, and refuses if the two differ. 24 Jev { input: String, wanted: Wanted, request: String, #[serde(default)] pick: Pick }, 25 /// The LLM's reply to `request`, from `model`. 26 Llm { model: String, request: String, #[serde(default)] pick: Pick }, 27 /// `input` was asked and got `answers`, one per question Jev answered. 28 /// `llm` is whether an LLM had to be asked; `listed` is whether the 29 /// question may be shown on the public feed. `who` is the asking 30 /// browser for this question only (`Asker`): a browser that asked it 31 /// before is not counted again. `None` when the browser sent no id. 32 /// `browser` is that browser's id itself, kept beside `who` so the 33 /// owner can see whose row it is. 34 Asked { 35 input: String, 36 answers: Vec<Answer>, 37 llm: bool, 38 listed: bool, 39 #[serde(default)] 40 who: Option<Asker>, 41 #[serde(default)] 42 browser: String, 43 }, 44 /// Someone cloned or pulled `repo` (`lmjtfy.git`) through the site. 45 /// Counted, and told to every open page. 46 Fetched { repo: String, fetch: Fetch }, 47 /// `who` thinks Jev's answer to `input` right (`Up`) or wrong (`Down`). 48 /// The vote is for the answer as it is kept now; the same vote again 49 /// takes it back. Answered with the `Rating` after it. 50 Rate { 51 input: String, 52 who: Asker, 53 vote: Vote, 54 #[serde(default)] 55 browser: String, 56 }, 57 /// The votes on Jev's answer to `input` as it is kept now, and `who`'s. 58 Rating { input: String, who: Option<Asker> }, 59 /// What the home page shows from the archive. 60 Home, 61 /// The next `FEED` listed questions asked before `after`, newest first: 62 /// the feed scrolled to its end. 63 Older { after: Cursor }, 64 /// What Jev said to `input`, if it was ever asked and answered. Nothing 65 /// is sent to find out. 66 Answer { input: String }, 67 /// Something happened, to be kept whole (`event.rs`). 68 Event(Box<Event>), 69} 70 71/// The archive object's name: there is one, for the whole site. A Worker 72/// that binds the archive (the site's, the admin's) reaches it by this. 73pub const OBJECT: &str = "everything"; 74 75/// The path of the archive's door for the owner's admin backend. The site's 76/// own Worker never sends a request there; the admin's Worker, which binds 77/// the same object from the owner's account, does. 78pub const ADMIN: &str = "/admin"; 79 80/// What the admin backend asks the archive. 81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 82pub enum Admin { 83 /// Rows of the archive's tables: one statement that only reads 84 /// (`reads_only`), with a value for each `?`. 85 Select { 86 sql: String, 87 #[serde(default)] 88 values: Vec<serde_json::Value>, 89 }, 90 /// The owner's say on whether `input` may be shown on the feed: yes, no, 91 /// or `None` to leave it to the rules again. Jev's own verdict 92 /// (`asked.listed`) is kept beside it, untouched. 93 Moderate { input: String, listed: Option<bool> }, 94} 95 96/// What the archive answers the admin backend. 97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 98pub enum Answered { 99 Rows { columns: Vec<String>, rows: Vec<Vec<serde_json::Value>> }, 100 Done, 101 Refused(String), 102} 103 104/// Whether `sql` is one statement that can only read. The admin's door 105/// changes the archive through `Admin`'s own messages, never through SQL: 106/// the schema changes only by a migration, and a response that was kept is 107/// never edited. Text in quotes is not looked at, so a question that says 108/// "delete" can still be searched for; pass such text as a value. 109pub fn reads_only(sql: &str) -> bool { 110 let mut bare = String::new(); 111 let mut quoted = None; 112 for c in sql.chars() { 113 match quoted { 114 Some(quote) if c == quote => quoted = None, 115 Some(_) => {} 116 None if c == '\'' || c == '"' => quoted = Some(c), 117 None => bare.push(c.to_ascii_lowercase()), 118 } 119 } 120 let bare = bare.trim().trim_end_matches(';').trim_end(); 121 let words: Vec<&str> = bare.split(|c: char| !c.is_ascii_alphanumeric() && c != '_').filter(|word| !word.is_empty()).collect(); 122 const WRITES: [&str; 13] = ["insert", "update", "delete", "replace", "drop", "alter", "create", "attach", "detach", "pragma", "vacuum", "reindex", "analyze"]; 123 quoted.is_none() && !bare.contains(';') && matches!(words.first(), Some(&"select") | Some(&"with")) && !words.iter().any(|word| WRITES.contains(word)) 124} 125 126/// Which of a request's kept responses an ask wants. A request is sent 127/// again only when a visitor asks for that (`Fresh`, the page's ↻); every 128/// response it ever got is kept, numbered from 1, newest last. 129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 130pub enum Pick { 131 /// The newest kept, or sent if there is none. 132 #[default] 133 Latest, 134 /// Sent now, whatever is kept, and kept as the newest. 135 Fresh, 136 /// That one, if kept; the newest kept otherwise. 137 Version(u32), 138 /// The newest kept, and never sent: `Called::NotKept` if there is none. 139 /// What a page that is stepping through old versions uses for the calls 140 /// after the one it stepped, so looking costs nothing. 141 KeptOnly, 142} 143 144/// Which version of each call a page wants, by `call_id`: the page's 145/// `$pins` signal, `id:3;id:fresh`, later entries winning. A call it does 146/// not name gets the newest kept. 147#[derive(Clone, Debug, Default, PartialEq, Eq)] 148pub struct Pins(pub Vec<(String, Pick)>); 149 150impl Pins { 151 pub fn parse(text: &str) -> Pins { 152 let mut pins: Vec<(String, Pick)> = Vec::new(); 153 for entry in text.split(';') { 154 let Some((id, what)) = entry.trim().split_once(':') else { continue }; 155 if id.len() != 12 || !id.bytes().all(|b| b.is_ascii_hexdigit()) { 156 continue; 157 } 158 let pick = match what { 159 "fresh" => Pick::Fresh, 160 number => match number.parse::<u32>() { 161 Ok(version) if version > 0 => Pick::Version(version), 162 _ => continue, 163 }, 164 }; 165 pins.retain(|(seen, _)| seen != id); 166 pins.push((id.to_owned(), pick)); 167 } 168 Pins(pins) 169 } 170 171 /// Stepping through old versions, and asking nothing new: a version is 172 /// pinned and nothing is to be sent again. Then a call that is not pinned 173 /// is only read, never sent, so looking costs nothing. 174 pub fn browsing(&self) -> bool { 175 self.0.iter().any(|(_, pick)| matches!(pick, Pick::Version(_))) && !self.0.iter().any(|(_, pick)| *pick == Pick::Fresh) 176 } 177 178 /// What to ask the archive for, for the call `id`. 179 pub fn pick(&self, id: &str) -> Pick { 180 match self.0.iter().find(|(seen, _)| seen == id) { 181 Some((_, pick)) => *pick, 182 None if self.browsing() => Pick::KeptOnly, 183 None => Pick::Latest, 184 } 185 } 186} 187 188/// A call, as the page names it for ↻ and ◀ ▶: the first 12 hex digits of 189/// the SHA-256 of where it went and what was sent. 190pub fn call_id(sent_to: &str, request: &str) -> String { 191 use sha2::{Digest, Sha256}; 192 let digest = Sha256::new().chain_update(sent_to.as_bytes()).chain_update([0]).chain_update(request.as_bytes()).finalize(); 193 digest.iter().take(6).map(|byte| format!("{byte:02x}")).collect() 194} 195 196/// A response the archive holds, and the call that got it. 197#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 198pub struct Record { 199 /// The response body exactly as it arrived. 200 pub response: String, 201 pub request_id: Option<String>, 202 pub attempts: u32, 203 /// The round trip of the call that was sent, in milliseconds. 204 pub took_ms: f64, 205 /// When that call was answered, in Unix milliseconds. 206 pub answered_ms: f64, 207 /// Whether this ask is the one that sent it. 208 pub sent_now: bool, 209 /// Which of the request's kept responses this is, from 1, and how many 210 /// there are. 211 #[serde(default = "first")] 212 pub version: u32, 213 #[serde(default = "first")] 214 pub versions: u32, 215} 216 217fn first() -> u32 { 218 1 219} 220 221impl Record { 222 pub fn sent(&self) -> Sent { 223 if self.sent_now { Sent::Now } else { Sent::Before { at_ms: self.answered_ms } } 224 } 225 226 /// The same record, as someone who did not send it sees it. 227 pub fn kept(mut self) -> Self { 228 self.sent_now = false; 229 self 230 } 231} 232 233/// A browser, for one question: the hex SHA-256 of the browser's random id 234/// and the question. It says whether this browser asked this question 235/// before, and nothing else: two questions from one browser give unrelated 236/// values, and the id cannot be had back from one. 237#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 238pub struct Asker(pub String); 239 240/// A browser's view of Jev's answer. 241#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 242pub enum Vote { 243 Up, 244 Down, 245} 246 247/// The votes on one answer, and the asking browser's own, if it voted. 248#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 249pub struct Rating { 250 pub up: u32, 251 pub down: u32, 252 pub mine: Option<Vote>, 253} 254 255/// Which shared budget refused a call. 256#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 257pub enum Pot { 258 Jev, 259 Llm, 260} 261 262/// What became of a call. 263#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 264pub enum Called { 265 Answered(Record), 266 /// Sent, or not sendable, and there is no response. Nothing is kept, so 267 /// the same request may be sent again. 268 Failed { error: String, request_id: Option<String>, took_ms: f64 }, 269 /// Not sent: today's budget for it is used up. 270 Spent(Pot), 271 /// Not sent, because the ask was `Pick::KeptOnly` and nothing is kept. 272 NotKept, 273} 274 275/// What Jev said to one question, as it is kept for the feed and for link 276/// previews: the answer in a few words, and the numbers behind it. 277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 278pub struct Answer { 279 /// What the page prints large: "No?", "Pepperoni.", "28%.". 280 pub headline: String, 281 pub detail: Detail, 282} 283 284/// The numbers behind an answer, by the type of question it answered. 285#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 286pub enum Detail { 287 /// Kept before details were. Only the headline is known. 288 Unknown, 289 /// A yes-or-no: the probability of yes. 290 Noul { p_yes: f64 }, 291 /// A how-likely, which Jev answers as a Noul about the thing itself: the 292 /// probability that it is so. 293 Chance { p: f64 }, 294 /// A pick: every option with its probability, most likely first. 295 Choice { confidence: f64, options: Vec<(String, f64)> }, 296 /// A how-much: every level with its probability, lowest first. 297 Score { score: f64, confidence: f64, levels: Vec<(String, f64)> }, 298} 299 300impl Answer { 301 /// The type and the numbers in one line, as the page prints them under 302 /// an answer. `None` when only the headline was kept. 303 pub fn note(&self) -> Option<String> { 304 Some(match &self.detail { 305 Detail::Unknown => return None, 306 Detail::Noul { p_yes } => format!("Noul · p(yes) = {p_yes:.2}"), 307 Detail::Chance { p } => format!("Noul · p = {p:.2} · Jev thinks that's {}", ask::likelihood(*p)), 308 Detail::Choice { confidence, .. } => format!("Choice · confidence {confidence:.2}"), 309 Detail::Score { score, confidence, levels } => { 310 format!("Score · {score:.1} of {} · confidence {confidence:.2}", levels.len().saturating_sub(1)) 311 } 312 }) 313 } 314 315 /// The `most` likeliest options or levels, likeliest first, for a line 316 /// of text. Empty for a Noul, whose one number is in the note. 317 pub fn spread(&self, most: usize) -> Vec<(&str, f64)> { 318 let mut spread: Vec<(&str, f64)> = match &self.detail { 319 Detail::Choice { options, .. } => options.iter().map(|(label, p)| (label.as_str(), *p)).collect(), 320 Detail::Score { levels, .. } => levels.iter().map(|(label, p)| (label.as_str(), *p)).collect(), 321 _ => Vec::new(), 322 }; 323 spread.sort_by(|a, b| b.1.total_cmp(&a.1)); 324 spread.truncate(most); 325 spread 326 } 327 328 /// Everything in one line: the headline, the note and the spread. 329 pub fn line(&self) -> String { 330 let mut parts = vec![self.headline.clone()]; 331 parts.extend(self.note()); 332 let spread: Vec<String> = self.spread(3).iter().map(|(label, p)| format!("{label} {:.0}%", p * 100.0)).collect(); 333 if !spread.is_empty() { 334 parts.push(spread.join(", ")); 335 } 336 parts.join(" · ") 337 } 338} 339 340/// A kept answer as it is stored: with its numbers, or, from before they 341/// were kept, as the headline alone. 342#[derive(Deserialize)] 343#[serde(untagged)] 344enum Stored { 345 Whole(Answer), 346 Headline(String), 347} 348 349/// Reads the answers a question was stored with, old form or new. 350pub fn stored(text: &str) -> Vec<Answer> { 351 let stored: Vec<Stored> = serde_json::from_str(text).unwrap_or_default(); 352 stored 353 .into_iter() 354 .map(|stored| match stored { 355 Stored::Whole(answer) => answer, 356 Stored::Headline(headline) => Answer { headline, detail: Detail::Unknown }, 357 }) 358 .collect() 359} 360 361/// A question somebody asked, and what Jev said. Nothing about who asked. 362#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 363pub struct Entry { 364 pub input: String, 365 pub answers: Vec<Answer>, 366 /// When it was last asked. 367 pub asked_ms: f64, 368 /// How many times it has been asked and answered. 369 pub times: u32, 370} 371 372impl Entry { 373 /// The answers in a few words each, as the feed prints them. 374 pub fn headlines(&self) -> String { 375 let headlines: Vec<&str> = self.answers.iter().map(|answer| answer.headline.as_str()).collect(); 376 headlines.join(" ") 377 } 378} 379 380/// How many questions the feed shows as asked lately, and as most asked. 381pub const FEED: u32 = 50; 382pub const MOST: u32 = 5; 383 384/// The archive in numbers. 385#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] 386pub struct Stats { 387 /// Different questions answered. 388 pub questions: u32, 389 /// Times a question was asked and answered, repeats included. 390 pub asks: u32, 391 /// Of the different questions, those Jev answered with no LLM. 392 pub no_llm: u32, 393 /// Requests that went out and were answered. 394 pub sent: u32, 395 /// Requests that did not go out, because their response was kept. 396 pub kept: u32, 397 /// Clones and pulls of the code through the site. 398 #[serde(default)] 399 pub clones: u32, 400 #[serde(default)] 401 pub pulls: u32, 402} 403 404impl Stats { 405 /// The share of questions answered with no LLM, as a whole percentage. 406 pub fn no_llm_percent(&self) -> Option<u32> { 407 (self.questions > 0).then(|| (f64::from(self.no_llm) * 100.0 / f64::from(self.questions)).round() as u32) 408 } 409} 410 411/// What the home page shows from the archive. The lists hold only questions 412/// that may be shown publicly; the numbers count every question. 413#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] 414pub struct Home { 415 /// Newest first. 416 pub lately: Vec<Entry>, 417 /// Asked more than once, most asked first. 418 pub most: Vec<Entry>, 419 pub stats: Stats, 420} 421 422/// Where the feed was scrolled to: the last question shown. Questions are 423/// listed newest first, ties broken by the question itself, so the next page 424/// starts strictly after this one and none is shown twice or skipped. 425#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 426pub struct Cursor { 427 pub asked_ms: f64, 428 pub input: String, 429} 430 431impl Entry { 432 /// Where the feed is once this entry is the last shown. 433 pub fn cursor(&self) -> Cursor { 434 Cursor { asked_ms: self.asked_ms, input: self.input.clone() } 435 } 436} 437 438/// What a fetch through the clone proxy was. Git's upload-pack request 439/// names the commits wanted; a pull also names the ones it has. 440#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 441pub enum Fetch { 442 Clone, 443 Pull, 444} 445 446impl Fetch { 447 /// Its name in the archive's `counts`, for one repository. 448 pub fn counted(self, repo: &str) -> String { 449 match self { 450 Fetch::Clone => format!("clone:{repo}"), 451 Fetch::Pull => format!("pull:{repo}"), 452 } 453 } 454} 455 456/// What the archive pushes to every open page over its socket, as JSON: 457/// `{"online": 3}` or `{"toast": "<html>"}`. 458#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 459#[serde(rename_all = "lowercase")] 460pub enum Live { 461 /// Pages open now. 462 Online(u32), 463 /// Something happened, as markup to show for a moment. 464 Toast(String), 465 /// The build the site is now (`LMJTFY_BUILD`), told to each page as it 466 /// connects. A page built from another offers a reload: a deploy 467 /// restarts the object, every page reconnects, and each hears this. 468 Build(String), 469 /// Elements that changed, each with an `id`: a page that has an element 470 /// with that id replaces it. An element with `data-prepend="<id>"` 471 /// instead puts its children at the top of that element, each removing 472 /// any element with its id first: a question asked again moves to the 473 /// top of the feed, and what a page has scrolled in below stays. The 474 /// activity feeds stay current this way, with no refresh. 475 Patch(String), 476} 477 478/// Where an open page connected from, as Cloudflare placed the request: a 479/// country code and a city, either unknown. It is kept on the page's socket 480/// while it is open, for the online list. 481#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 482pub struct Place { 483 pub country: Option<String>, 484 pub city: Option<String>, 485} 486 487impl Place { 488 /// From what the Worker passed on, kept only if it looks like a place: 489 /// a country is two letters, a city a short line of text. 490 pub fn new(country: Option<&str>, city: Option<&str>) -> Place { 491 let country = country 492 .map(str::trim) 493 .filter(|code| code.len() == 2 && code.bytes().all(|b| b.is_ascii_alphabetic())) 494 .map(str::to_ascii_uppercase) 495 // Cloudflare's own codes for Tor and for unknown are not places. 496 .filter(|code| code != "T1" && code != "XX"); 497 let city = city 498 .map(str::trim) 499 .filter(|city| !city.is_empty() && city.chars().count() <= 64 && !city.chars().any(char::is_control)) 500 .map(str::to_owned); 501 Place { country, city } 502 } 503 504 /// The country's flag: its two letters as regional indicator symbols. 505 pub fn flag(&self) -> Option<String> { 506 let code = self.country.as_deref()?; 507 code.chars().map(|letter| char::from_u32(0x1F1E6 + (letter as u32 - 'A' as u32))).collect() 508 } 509 510 /// `Austin, US`, `US`, or `somewhere`. 511 pub fn label(&self) -> String { 512 match (&self.city, &self.country) { 513 (Some(city), Some(country)) => format!("{city}, {country}"), 514 (Some(city), None) => city.clone(), 515 (None, Some(country)) => country.clone(), 516 (None, None) => "somewhere".to_owned(), 517 } 518 } 519} 520 521/// A header value as the Worker percent-encoded it (`+` for a space), back 522/// to text. `None` if it is not UTF-8 or a `%` is not followed by two hex 523/// digits. 524pub fn percent_decoded(text: &str) -> Option<String> { 525 let mut bytes = Vec::with_capacity(text.len()); 526 let mut rest = text.bytes(); 527 while let Some(byte) = rest.next() { 528 match byte { 529 b'+' => bytes.push(b' '), 530 b'%' => { 531 let hex = [rest.next()?, rest.next()?]; 532 bytes.push(u8::from_str_radix(std::str::from_utf8(&hex).ok()?, 16).ok()?); 533 } 534 byte => bytes.push(byte), 535 } 536 } 537 String::from_utf8(bytes).ok() 538} 539 540/// What the archive keeps on an open page's socket: where it is, and whether 541/// a Worker that passes places on sent it. A page that reconnects while a 542/// deploy is still reaching Cloudflare's edge can come through the Worker 543/// from before, which passes no place; `passed` is false for it, and the 544/// archive asks it to reconnect once the deploy has settled. 545#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] 546pub struct Seen { 547 pub place: Place, 548 pub passed: bool, 549 /// When it connected, in Unix milliseconds. 550 pub at_ms: f64, 551 /// The row of `events` that says it connected, which the row that says 552 /// it left copies who it was from. 553 #[serde(default)] 554 pub event: Option<f64>, 555} 556 557/// How long after connecting an unplaced page is asked to reconnect: long 558/// enough for a deploy to reach the whole edge. 559pub const REPLACE_AFTER_MS: f64 = 10_000.0; 560 561impl Seen { 562 /// Whether this page should reconnect to be placed: it came through a 563 /// Worker that passes no place, long enough ago. A socket with nothing 564 /// kept on it (`None`) was accepted by code from before places. 565 pub fn stale(seen: Option<&Seen>, now_ms: f64) -> bool { 566 seen.is_none_or(|seen| !seen.passed && now_ms - seen.at_ms >= REPLACE_AFTER_MS) 567 } 568} 569 570/// How many open pages are in each place, most first, then by name. 571pub fn places(open: impl IntoIterator<Item = Place>) -> Vec<(Place, u32)> { 572 let mut counted: std::collections::BTreeMap<Place, u32> = std::collections::BTreeMap::new(); 573 for place in open { 574 *counted.entry(place).or_default() += 1; 575 } 576 let mut counted: Vec<(Place, u32)> = counted.into_iter().collect(); 577 counted.sort_by(|(a, n), (b, m)| m.cmp(n).then_with(|| a.label().cmp(&b.label()))); 578 counted 579} 580 581/// What the archive says back. 582#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 583pub enum Told { 584 Called(Called), 585 Noted, 586 Home(Home), 587 Answer(Option<Entry>), 588 Older(Vec<Entry>), 589 /// `None` if the question was never answered, so there is nothing to 590 /// vote on. 591 Rating(Option<Rating>), 592} 593 594/// A moment as `YYYY-MM-DD HH:MM UTC`. 595pub fn when(unix_ms: f64) -> String { 596 let minutes = (unix_ms / 60_000.0).floor().max(0.0) as u64; 597 format!("{} {:02}:{:02} UTC", budget::date(budget::day(unix_ms)), minutes / 60 % 24, minutes % 60) 598} 599 600#[cfg(test)] 601mod tests;