diff --git a/4-Infrastructure/infra/ene-session-sync/src/wiki.rs b/4-Infrastructure/infra/ene-session-sync/src/wiki.rs index 7873a8c7..a572aa47 100644 --- a/4-Infrastructure/infra/ene-session-sync/src/wiki.rs +++ b/4-Infrastructure/infra/ene-session-sync/src/wiki.rs @@ -18,6 +18,28 @@ pub const MAX_TITLE_LEN: usize = 160; /// Maximum allowed content size in bytes (256 KB). pub const MAX_CONTENT_BYTES: usize = 262_144; +// ── Node/provenance defaults (configurable) ──────────────────────────────────── + +#[derive(Debug, Clone)] +struct ProvenanceDefaults { + node: String, + lake_seed: String, + tailscale_ip: String, +} + +fn provenance_defaults() -> ProvenanceDefaults { + // These defaults are intentionally conservative; callers should set env vars + // in production. + let node = std::env::var("ENE_NODE_ID").unwrap_or_else(|_| "ene".to_string()); + let lake_seed = std::env::var("ENE_LAKE_SEED").unwrap_or_else(|_| "local".to_string()); + let tailscale_ip = std::env::var("ENE_TAILSCALE_IP").unwrap_or_else(|_| "".to_string()); + ProvenanceDefaults { + node, + lake_seed, + tailscale_ip, + } +} + // ── Structs ──────────────────────────────────────────────────────────────────── /// A wiki page header (latest-revision summary, no body text). @@ -49,10 +71,7 @@ pub struct WikiRevision { /// Trim and collapse whitespace in a wiki title; reject empty or oversized titles. pub fn normalize_title(title: &str) -> Result { - let cleaned: String = title - .split_whitespace() - .collect::>() - .join(" "); + let cleaned: String = title.split_whitespace().collect::>().join(" "); if cleaned.is_empty() { return Err(anyhow!("wiki title is required")); } @@ -126,7 +145,6 @@ pub fn canonical_json(v: &Value) -> String { fn to_sorted(v: &Value) -> Value { match v { Value::Object(map) => { - // Collect into a BTreeMap so keys are sorted lexicographically. let sorted: std::collections::BTreeMap<_, _> = map .iter() .map(|(k, vv)| (k.clone(), to_sorted(vv))) @@ -145,7 +163,6 @@ pub fn canonical_json(v: &Value) -> String { serde_json::to_string(&sorted).unwrap_or_else(|_| "null".to_string()) } -/// Manual `[[...]]` span parser — returns `(inner_text, start_byte)` for each span. fn wiki_spans(text: &str) -> Vec<&str> { let bytes = text.as_bytes(); let len = bytes.len(); @@ -153,12 +170,10 @@ fn wiki_spans(text: &str) -> Vec<&str> { let mut i = 0; while i + 1 < len { if bytes[i] == b'[' && bytes[i + 1] == b'[' { - // Find the matching `]]`. let start = i + 2; let mut j = start; while j + 1 < len { if bytes[j] == b']' && bytes[j + 1] == b']' { - // Reject spans that contain newlines or nested brackets. let inner = &text[start..j]; if !inner.contains('\n') && !inner.contains('[') && !inner.contains(']') { spans.push(inner); @@ -169,7 +184,6 @@ fn wiki_spans(text: &str) -> Vec<&str> { j += 1; } if j + 1 >= len { - // Unclosed span — advance past the opening `[[`. i += 2; } } else { @@ -179,20 +193,11 @@ fn wiki_spans(text: &str) -> Vec<&str> { spans } -/// Extract the link target from a raw `[[...]]` inner string. -/// -/// Strip everything from the first `|` or `#` onward. fn link_target(inner: &str) -> &str { - let cut = inner - .find(|c| c == '|' || c == '#') - .unwrap_or(inner.len()); + let cut = inner.find(|c| c == '|' || c == '#').unwrap_or(inner.len()); &inner[..cut] } -/// Extract all wiki links from `text`. -/// -/// Returns deduplicated, case-insensitively sorted normalized titles. -/// Skips spans whose target (after normalization) starts with `category:`. pub fn extract_links(text: &str) -> Vec { let mut links: Vec = Vec::new(); for inner in wiki_spans(text) { @@ -206,26 +211,18 @@ pub fn extract_links(text: &str) -> Vec { } } } - // Deduplicate. links.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); links.dedup_by(|a, b| a.to_lowercase() == b.to_lowercase()); links } -/// Extract all `[[Category:...]]` entries from `text`. -/// -/// Returns deduplicated, case-insensitively sorted category names (prefix stripped). pub fn extract_categories(text: &str) -> Vec { let mut cats: Vec = Vec::new(); for inner in wiki_spans(text) { let target = link_target(inner).trim(); - // Accept `Category:Foo` or `category:foo` (case-insensitive prefix). let lower = target.to_lowercase(); - // The Python CATEGORY_RE also allows whitespace: `Category : Foo`. - // We replicate that by checking the prefix after collapsing whitespace. let condensed: String = target.split_whitespace().collect::>().join(" "); let cond_lower = condensed.to_lowercase(); - // Match `category :` or `category:` prefix. let remainder = if cond_lower.starts_with("category :") { Some(condensed["category :".len()..].trim().to_string()) } else if lower.starts_with("category:") { @@ -244,10 +241,6 @@ pub fn extract_categories(text: &str) -> Vec { cats } -/// Compute the deterministic write receipt for a revision. -/// -/// Receipt = SHA-256 hex of canonical JSON of -/// `{"author":…,"created_at":…,"revision":…,"slug":…,"text_sha256":…}`. pub fn write_receipt( slug: &str, revision: i64, @@ -256,7 +249,6 @@ pub fn write_receipt( created_at_ms: i64, ) -> String { let text_sha256 = sha256_hex(text.as_bytes()); - // Build a BTreeMap so keys are sorted identically to Python's sort_keys=True. let mut map = std::collections::BTreeMap::new(); map.insert("author", serde_json::Value::String(author.to_string())); map.insert( @@ -273,7 +265,6 @@ pub fn write_receipt( sha256_hex(payload.as_bytes()) } -/// Count occurrences of `needle` (as a plain substring) in `haystack`. #[inline] fn count_substr(haystack: &str, needle: &str) -> usize { if needle.is_empty() { @@ -288,9 +279,6 @@ fn count_substr(haystack: &str, needle: &str) -> usize { count } -/// Count distinct "word-like" tokens matching `[a-zA-Z][a-zA-Z0-9_]{2,}`. -/// -/// This replicates `len(set(re.findall(r"[a-zA-Z][a-zA-Z0-9_]{2,}", lowered)))`. fn distinct_word_tokens(text: &str) -> usize { let bytes = text.as_bytes(); let len = bytes.len(); @@ -298,7 +286,6 @@ fn distinct_word_tokens(text: &str) -> usize { let mut i = 0; while i < len { let b = bytes[i]; - // Must start with a letter (a-z after lowering). if b.is_ascii_alphabetic() { let start = i; i += 1; @@ -306,7 +293,6 @@ fn distinct_word_tokens(text: &str) -> usize { i += 1; } let word = &text[start..i]; - // Must be at least 3 characters (1 leading + at least 2 more). if word.len() >= 3 { tokens.insert(word.to_string()); } @@ -317,24 +303,7 @@ fn distinct_word_tokens(text: &str) -> usize { tokens.len() } -/// Derive a deterministic 14-axis concept vector for a wiki page. -/// -/// Axes 0,1,3,4,8,9,10 are always 0.0. Active axes: -/// - axis 2 : topology / manifold / links -/// - axis 5 : hash / receipt / verify -/// - axis 6 : sqlite / schema / index -/// - axis 7 : lexical richness (distinct tokens / 500) -/// - axis 11 : proof / lean / theorem -/// - axis 12 : categories / archive / history -/// - axis 13 : author / provenance / attest -/// -/// The vector is L2-normalised then rounded to 6 decimal places. -pub fn concept_vector_for_wiki( - title: &str, - text: &str, - links: &[String], - categories: &[String], -) -> Vec { +pub fn concept_vector_for_wiki(title: &str, text: &str, links: &[String], categories: &[String]) -> Vec { let combined = format!("{}\n{}", title, text).to_lowercase(); let mut axes = vec![0.0f64; 14]; @@ -382,31 +351,18 @@ pub fn concept_vector_for_wiki( / 8.0, ); - // If all axes are zero, set the lexical richness axis to 1. if axes.iter().all(|&x| x == 0.0) { axes[7] = 1.0; } - // L2 normalise. let norm = axes.iter().map(|x| x * x).sum::().sqrt(); axes.iter() - .map(|&x| { - if norm > 0.0 { - // Round to 6 decimal places. - (x / norm * 1_000_000.0).round() / 1_000_000.0 - } else { - 0.0 - } - }) + .map(|&x| if norm > 0.0 { (x / norm * 1_000_000.0).round() / 1_000_000.0 } else { 0.0 }) .collect() } -/// Bin a concept vector into 0–7 genome integers and return the six named axes. pub fn genome_from_vector(v: &[f64]) -> Value { - let bins: Vec = v - .iter() - .map(|&x| u8::min(7, (x * 8.0) as u8)) - .collect(); + let bins: Vec = v.iter().map(|&x| u8::min(7, (x * 8.0) as u8)).collect(); let get = |idx: usize| -> i64 { bins.get(idx).copied().unwrap_or(0) as i64 }; json!({ "mu": get(1), @@ -418,16 +374,10 @@ pub fn genome_from_vector(v: &[f64]) -> Value { }) } -/// Build the archive_id string for a slug + content_hash pair. fn archive_id_for(slug: &str, content_hash: &str) -> String { - format!( - "json_catalog_ene_wiki_{}_{}", - slug, - &content_hash[..content_hash.len().min(16)] - ) + format!("json_catalog_ene_wiki_{}_{}", slug, &content_hash[..content_hash.len().min(16)]) } -/// Build the full archive record dict matching the Python `make_archive_record()`. pub fn make_archive_record( title: &str, slug: &str, @@ -466,20 +416,15 @@ pub fn make_archive_record( }) } -/// Build the JSONL event dict matching the Python `make_jsonl_event()`. pub fn make_jsonl_event(record: &Value, concept_vector: &[f64], receipt: &str) -> Value { + let defaults = provenance_defaults(); + let now_ms = now_unix_ms() as f64 / 1_000.0; let raw = &record["raw_content"]; let slug = raw["slug"].as_str().unwrap_or(""); let title = raw["title"].as_str().unwrap_or(""); - let links_len = raw["links"] - .as_array() - .map(|a| a.len()) - .unwrap_or(0); - let categories_arr: Vec = raw["categories"] - .as_array() - .cloned() - .unwrap_or_default(); + let links_len = raw["links"].as_array().map(|a| a.len()).unwrap_or(0); + let categories_arr: Vec = raw["categories"].as_array().cloned().unwrap_or_default(); let content_hash = record["content_hash"].as_str().unwrap_or(""); let extracted_at = record["extracted_at"].as_str().unwrap_or(""); let archive_id = record["archive_id"].as_str().unwrap_or(""); @@ -488,14 +433,10 @@ pub fn make_jsonl_event(record: &Value, concept_vector: &[f64], receipt: &str) - let pkg = format!("ene/wiki/{}", slug); let event_id = format!("ene:{}", archive_id); - // bind.cost = max(1, len(extracted_text.encode("utf-8"))) << 16 let text_bytes = extracted_text.len().max(1); let cost: i64 = (text_bytes as i64) << 16; - let mut tags = vec![ - Value::String("ene".to_string()), - Value::String("wiki".to_string()), - ]; + let mut tags = vec![Value::String("ene".to_string()), Value::String("wiki".to_string())]; for cat in &categories_arr { tags.push(cat.clone()); } @@ -540,18 +481,15 @@ pub fn make_jsonl_event(record: &Value, concept_vector: &[f64], receipt: &str) - "class": "informational_bind", }, "provenance": { - "node": "ene-wiki-layer", - "lake_seed": "local", - "tailscale_ip": "", + "node": defaults.node, + "lake_seed": defaults.lake_seed, + "tailscale_ip": defaults.tailscale_ip, "attestation_hash": format!("sha256:{}", receipt), "prev_id": Value::Null, }, }) } -/// Return `true` if the text passes the content admission gate. -/// -/// Rejects: oversized text, ` bool { if text.len() >= MAX_CONTENT_BYTES { return false; @@ -565,7 +503,6 @@ pub fn admit_write(text: &str) -> bool { true } -/// Current Unix time as milliseconds. fn now_unix_ms() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -575,13 +512,11 @@ fn now_unix_ms() -> i64 { // ── ENEWikiLayer (SQLite) ────────────────────────────────────────────────────── -/// SQLite-backed wiki layer. Drop-in equivalent of `ene_wiki_layer.ENEWikiLayer`. pub struct ENEWikiLayer { pub db_path: PathBuf, } impl ENEWikiLayer { - /// Open (or create) a wiki backed by the SQLite database at `db_path`. pub fn new(db_path: impl AsRef) -> Result { let db_path = db_path.as_ref().to_path_buf(); if let Some(parent) = db_path.parent() { @@ -593,7 +528,6 @@ impl ENEWikiLayer { Ok(layer) } - /// Open a raw SQLite connection to the backing database. fn open(&self) -> Result { let conn = rusqlite::Connection::open(&self.db_path) .with_context(|| format!("opening sqlite db {:?}", self.db_path))?; @@ -601,7 +535,6 @@ impl ENEWikiLayer { Ok(conn) } - /// Create all tables if they do not exist. pub fn init_tables(&self) -> Result<()> { let conn = self.open()?; conn.execute_batch( @@ -663,7 +596,6 @@ impl ENEWikiLayer { ) .context("creating wiki tables")?; - // Ensure optional columns exist on ene_wiki_revisions (schema migration). let extra_cols = [ ("archive_id", "TEXT"), ("content_hash", "TEXT"), @@ -671,25 +603,21 @@ impl ENEWikiLayer { ("jsonl_event", "TEXT"), ]; let existing: Vec = { - let mut stmt = conn - .prepare("PRAGMA table_info(ene_wiki_revisions)")?; - let cols: Vec = stmt.query_map([], |row| row.get::<_, String>(1))? + let mut stmt = conn.prepare("PRAGMA table_info(ene_wiki_revisions)")?; + let cols: Vec = stmt + .query_map([], |row| row.get::<_, String>(1))? .filter_map(|r| r.ok()) .collect(); cols }; for (col, decl) in &extra_cols { if !existing.contains(&col.to_string()) { - conn.execute_batch(&format!( - "ALTER TABLE ene_wiki_revisions ADD COLUMN {} {}", - col, decl - ))?; + conn.execute_batch(&format!("ALTER TABLE ene_wiki_revisions ADD COLUMN {} {}", col, decl))?; } } Ok(()) } - /// Upsert a packages row from a JSONL event. fn upsert_package(conn: &rusqlite::Connection, event: &Value) -> Result<()> { let data = &event["data"]; let pkg = data["pkg"].as_str().unwrap_or(""); @@ -739,14 +667,7 @@ impl ENEWikiLayer { Ok(()) } - /// Write or update a wiki page, appending a new revision. - pub fn put_page( - &self, - title: &str, - text: &str, - author: &str, - summary: &str, - ) -> Result { + pub fn put_page(&self, title: &str, text: &str, author: &str, summary: &str) -> Result { if !admit_write(text) { return Err(anyhow!("wiki write rejected: content failed admission gate")); } @@ -756,7 +677,6 @@ impl ENEWikiLayer { let conn = self.open()?; - // Determine next revision number. let latest: Option = conn .query_row( "SELECT latest_revision FROM ene_wiki_pages WHERE slug = ?1", @@ -786,11 +706,9 @@ impl ENEWikiLayer { let archive_id = archive_record["archive_id"].as_str().unwrap_or("").to_string(); let content_hash = archive_record["content_hash"].as_str().unwrap_or("").to_string(); - let archive_record_json = - serde_json::to_string(&archive_record).unwrap_or_default(); + let archive_record_json = serde_json::to_string(&archive_record).unwrap_or_default(); let jsonl_event_json = serde_json::to_string(&jsonl_event).unwrap_or_default(); - // Insert revision. conn.execute( r#" INSERT INTO ene_wiki_revisions @@ -807,7 +725,6 @@ impl ENEWikiLayer { ) .context("inserting wiki revision")?; - // Upsert page header. conn.execute( r#" INSERT INTO ene_wiki_pages (slug, title, latest_revision, updated_at, receipt) @@ -822,24 +739,16 @@ impl ENEWikiLayer { ) .context("upserting wiki page")?; - // Replace links. - conn.execute( - "DELETE FROM ene_wiki_links WHERE slug = ?1", - rusqlite::params![&slug], - )?; + conn.execute("DELETE FROM ene_wiki_links WHERE slug = ?1", rusqlite::params![&slug])?; for link_title in &links { let target_slug = title_slug(link_title).unwrap_or_default(); conn.execute( - r#" - INSERT OR IGNORE INTO ene_wiki_links (slug, target_slug, target_title) - VALUES (?1,?2,?3) - "#, + r#"INSERT OR IGNORE INTO ene_wiki_links (slug, target_slug, target_title) VALUES (?1,?2,?3)"#, rusqlite::params![&slug, &target_slug, link_title], ) .context("inserting wiki link")?; } - // Replace categories. conn.execute( "DELETE FROM ene_wiki_categories WHERE slug = ?1", rusqlite::params![&slug], @@ -868,12 +777,10 @@ impl ENEWikiLayer { }) } - /// Retrieve the latest revision of a page by title, or `None` if not found. pub fn get_page(&self, title: &str) -> Result> { let slug = title_slug(title)?; let conn = self.open()?; - // Find latest revision number from the pages table. let latest: Option = conn .query_row( "SELECT latest_revision FROM ene_wiki_pages WHERE slug = ?1", @@ -887,14 +794,9 @@ impl ENEWikiLayer { None => return Ok(None), }; - // Fetch the revision body. let row: Option<(String, String, i64, String, String, String, i64, String)> = conn .query_row( - r#" - SELECT title, slug, revision, text, author, summary, created_at, receipt - FROM ene_wiki_revisions - WHERE slug = ?1 AND revision = ?2 - "#, + r#"SELECT title, slug, revision, text, author, summary, created_at, receipt FROM ene_wiki_revisions WHERE slug = ?1 AND revision = ?2"#, rusqlite::params![&slug, revision], |r| { Ok(( @@ -912,13 +814,11 @@ impl ENEWikiLayer { .optional() .context("fetching wiki revision")?; - let (title_db, slug_db, rev, text, author, summary, created_at_ms, receipt_db) = - match row { - Some(t) => t, - None => return Ok(None), - }; + let (title_db, slug_db, rev, text, author, summary, created_at_ms, receipt_db) = match row { + Some(t) => t, + None => return Ok(None), + }; - // Fetch links. let mut stmt = conn.prepare( "SELECT target_title FROM ene_wiki_links WHERE slug = ?1 ORDER BY lower(target_title)", )?; @@ -927,7 +827,6 @@ impl ENEWikiLayer { .filter_map(|r| r.ok()) .collect(); - // Fetch categories. let mut stmt = conn.prepare( "SELECT category FROM ene_wiki_categories WHERE slug = ?1 ORDER BY lower(category)", )?; @@ -950,19 +849,12 @@ impl ENEWikiLayer { })) } - /// Search pages by title or slug containing `query` (case-insensitive LIKE). pub fn search(&self, query: &str, limit: i64) -> Result> { let limit = limit.max(1).min(100); let term = format!("%{}%", query.trim()); let conn = self.open()?; let mut stmt = conn.prepare( - r#" - SELECT slug, title, latest_revision, updated_at, receipt - FROM ene_wiki_pages - WHERE title LIKE ?1 OR slug LIKE ?1 - ORDER BY updated_at DESC - LIMIT ?2 - "#, + r#"SELECT slug, title, latest_revision, updated_at, receipt FROM ene_wiki_pages WHERE title LIKE ?1 OR slug LIKE ?1 ORDER BY updated_at DESC LIMIT ?2"#, )?; let pages: Vec = stmt .query_map(rusqlite::params![&term, limit], |r| { @@ -979,18 +871,11 @@ impl ENEWikiLayer { Ok(pages) } - /// Return slugs of all pages that link *to* `title` via `ene_wiki_links`. pub fn backlinks(&self, title: &str) -> Result> { let target_slug = title_slug(title)?; let conn = self.open()?; let mut stmt = conn.prepare( - r#" - SELECT l.slug - FROM ene_wiki_links l - JOIN ene_wiki_pages p ON p.slug = l.slug - WHERE l.target_slug = ?1 - ORDER BY lower(p.title) - "#, + r#"SELECT l.slug FROM ene_wiki_links l JOIN ene_wiki_pages p ON p.slug = l.slug WHERE l.target_slug = ?1 ORDER BY lower(p.title)"#, )?; let slugs: Vec = stmt .query_map(rusqlite::params![&target_slug], |r| r.get(0))? @@ -999,19 +884,11 @@ impl ENEWikiLayer { Ok(slugs) } - /// Return the most recently changed revisions, newest first. pub fn recent_changes(&self, limit: i64) -> Result> { let limit = limit.max(1).min(100); let conn = self.open()?; let mut stmt = conn.prepare( - r#" - SELECT r.slug, r.title, r.revision, r.text, r.author, r.summary, - r.created_at, r.receipt - FROM ene_wiki_revisions r - JOIN ene_wiki_pages p ON p.slug = r.slug AND p.latest_revision = r.revision - ORDER BY r.created_at DESC - LIMIT ?1 - "#, + r#"SELECT r.slug, r.title, r.revision, r.text, r.author, r.summary, r.created_at, r.receipt FROM ene_wiki_revisions r JOIN ene_wiki_pages p ON p.slug = r.slug AND p.latest_revision = r.revision ORDER BY r.created_at DESC LIMIT ?1"#, )?; let revs: Vec = stmt .query_map(rusqlite::params![limit], |r| { @@ -1034,30 +911,21 @@ impl ENEWikiLayer { } } -// ── RdsWikiLayer (PostgreSQL / tokio-postgres) ───────────────────────────────── - -/// PostgreSQL-backed wiki layer. Drop-in equivalent of `ene_rds_wiki_layer.ENERDSWikiLayer`. pub struct RdsWikiLayer { pub dsn: String, } impl RdsWikiLayer { - /// Create an `RdsWikiLayer` using the given DSN string. - /// - /// Does NOT open a persistent connection — each operation creates a short-lived - /// connection, matching the Python psycopg2 pattern. pub async fn new(dsn: &str) -> Result { Ok(Self { dsn: dsn.to_string(), }) } - /// Open a tokio-postgres connection pair (client + connection driver). async fn connect(&self) -> Result { let (client, connection) = tokio_postgres::connect(&self.dsn, tokio_postgres::NoTls) .await .context("connecting to PostgreSQL")?; - // Drive the connection in a detached task. tokio::spawn(async move { if let Err(e) = connection.await { tracing::warn!("postgres connection error: {}", e); @@ -1066,7 +934,6 @@ impl RdsWikiLayer { Ok(client) } - /// Create all tables in the `ene` schema if they do not exist. pub async fn init_tables(&self) -> Result<()> { let client = self.connect().await?; client @@ -1134,14 +1001,7 @@ impl RdsWikiLayer { Ok(()) } - /// Write or update a wiki page, appending a new revision. - pub async fn put_page( - &self, - title: &str, - text: &str, - author: &str, - summary: &str, - ) -> Result { + pub async fn put_page(&self, title: &str, text: &str, author: &str, summary: &str) -> Result { if !admit_write(text) { return Err(anyhow!("wiki write rejected: content failed admission gate")); } @@ -1151,7 +1011,6 @@ impl RdsWikiLayer { let client = self.connect().await?; - // Determine next revision. let latest: Option = { let row = client .query_opt( @@ -1184,7 +1043,6 @@ impl RdsWikiLayer { let archive_id = archive_record["archive_id"].as_str().unwrap_or("").to_string(); let content_hash = archive_record["content_hash"].as_str().unwrap_or("").to_string(); - // Insert revision. client .execute( r#" @@ -1211,7 +1069,6 @@ impl RdsWikiLayer { .await .context("inserting wiki revision")?; - // Upsert page header. client .execute( r#" @@ -1228,29 +1085,20 @@ impl RdsWikiLayer { .await .context("upserting wiki page")?; - // Replace links. client - .execute( - "DELETE FROM ene.wiki_links WHERE slug = $1", - &[&slug], - ) + .execute("DELETE FROM ene.wiki_links WHERE slug = $1", &[&slug]) .await?; for link_title in &links { let target_slug = title_slug(link_title).unwrap_or_default(); client .execute( - r#" - INSERT INTO ene.wiki_links (slug, target_slug, target_title) - VALUES ($1,$2,$3) - ON CONFLICT DO NOTHING - "#, + r#"INSERT INTO ene.wiki_links (slug, target_slug, target_title) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING"#, &[&slug, &target_slug, link_title], ) .await .context("inserting wiki link")?; } - // Replace categories. client .execute( "DELETE FROM ene.wiki_categories WHERE slug = $1", @@ -1267,7 +1115,6 @@ impl RdsWikiLayer { .context("inserting wiki category")?; } - // Upsert packages. let data = &jsonl_event["data"]; let pkg = data["pkg"].as_str().unwrap_or(""); let version = data["version"].as_str().unwrap_or(""); @@ -1336,17 +1183,12 @@ impl RdsWikiLayer { }) } - /// Retrieve the latest revision of a page by title, or `None` if not found. pub async fn get_page(&self, title: &str) -> Result> { let slug = title_slug(title)?; let client = self.connect().await?; - // Find latest revision number. let latest = client - .query_opt( - "SELECT latest_revision FROM ene.wiki_pages WHERE slug = $1", - &[&slug], - ) + .query_opt("SELECT latest_revision FROM ene.wiki_pages WHERE slug = $1", &[&slug]) .await .context("querying wiki page")?; let revision: i64 = match latest { @@ -1354,14 +1196,9 @@ impl RdsWikiLayer { None => return Ok(None), }; - // Fetch the revision body. let row = client .query_opt( - r#" - SELECT title, slug, revision, text, author, summary, created_at_ms, receipt - FROM ene.wiki_revisions - WHERE slug = $1 AND revision = $2 - "#, + r#"SELECT title, slug, revision, text, author, summary, created_at_ms, receipt FROM ene.wiki_revisions WHERE slug = $1 AND revision = $2"#, &[&slug, &revision], ) .await @@ -1374,7 +1211,6 @@ impl RdsWikiLayer { let slug_db: String = row.get(1); - // Fetch links. let link_rows = client .query( "SELECT target_title FROM ene.wiki_links WHERE slug = $1 ORDER BY lower(target_title)", @@ -1383,7 +1219,6 @@ impl RdsWikiLayer { .await?; let links: Vec = link_rows.iter().map(|r| r.get(0)).collect(); - // Fetch categories. let cat_rows = client .query( "SELECT category FROM ene.wiki_categories WHERE slug = $1 ORDER BY lower(category)", @@ -1407,9 +1242,6 @@ impl RdsWikiLayer { } } -// ── Rusqlite optional helper ─────────────────────────────────────────────────── - -/// Extension trait to turn "not found" rusqlite errors into `Option::None`. trait OptionalExt { fn optional(self) -> Result>; }