lantern 0.2.2

Local-first, provenance-aware semantic search for agent activity
Documentation
//! Display full provenance and chunk text for a single indexed source.
//!
//! `show` accepts either a full source id or an unambiguous prefix (like
//! git's short hashes) so agents and humans can copy the 32-character id
//! printed by inspect or trim it down for interactive use. The rendered
//! output reuses `export::ExportedSource` so JSON consumers see the same
//! shape they'd get from a single-source `export`.

use anyhow::{Context, Result};
use rusqlite::params;

use crate::export::{ExportedSource, load_source};
use crate::inspect::{ago, now_unix};
use crate::store::Store;

pub fn show(store: &Store, id_or_prefix: &str) -> Result<ExportedSource> {
    let trimmed = id_or_prefix.trim();
    if trimmed.is_empty() {
        anyhow::bail!("source id must not be empty");
    }

    let conn = store.conn();
    let candidates: Vec<String> = {
        let mut stmt =
            conn.prepare("SELECT id FROM sources WHERE id LIKE ?1 || '%' ORDER BY id LIMIT 2")?;
        let rows = stmt.query_map(params![trimmed], |row| row.get::<_, String>(0))?;
        rows.collect::<Result<Vec<_>, _>>()?
    };

    match candidates.len() {
        0 => anyhow::bail!("no source matches id {trimmed:?}"),
        1 => load_source(conn, &candidates[0])
            .with_context(|| format!("loading source {}", candidates[0])),
        _ => anyhow::bail!(
            "source id {trimmed:?} is ambiguous; try a longer prefix (matched at least {} sources)",
            candidates.len()
        ),
    }
}

fn chunk_metadata_line(chunk: &crate::export::ExportedChunk) -> Option<String> {
    let mut parts = Vec::new();
    if let Some(role) = &chunk.role {
        parts.push(format!("role={role}"));
    }
    if let Some(session_id) = &chunk.session_id {
        parts.push(format!("session={session_id}"));
    }
    if let Some(turn_id) = &chunk.turn_id {
        parts.push(format!("turn={turn_id}"));
    }
    if let Some(tool_name) = &chunk.tool_name {
        parts.push(format!("tool={tool_name}"));
    }
    if let Some(ts) = chunk.timestamp_unix {
        parts.push(format!("ts={ts}"));
    }
    if parts.is_empty() {
        None
    } else {
        Some(parts.join(" "))
    }
}

pub fn print_text(source: &ExportedSource) {
    println!("source:   {}", source.source_id);
    println!("uri:      {}", source.uri);
    if let Some(p) = &source.path {
        println!("path:     {p}");
    }
    println!("kind:     {}", source.kind);
    println!("bytes:    {}", source.bytes);
    println!("sha256:   {}", source.content_sha256);
    let now = now_unix();
    println!(
        "ingested: {} ({})",
        source.ingested_at,
        ago(now, source.ingested_at)
    );
    if let Some(m) = source.mtime_unix {
        println!("mtime:    {m} ({})", ago(now, m));
    }
    println!("chunks:   {}", source.chunks.len());

    for chunk in &source.chunks {
        println!();
        println!(
            "--- chunk {ord} [bytes {start}..{end}, chars {chars}, sha {sha}] ---",
            ord = chunk.ordinal,
            start = chunk.byte_start,
            end = chunk.byte_end,
            chars = chunk.char_count,
            sha = &chunk.sha256[..12.min(chunk.sha256.len())],
        );
        if let Some(meta) = chunk_metadata_line(chunk) {
            println!("{meta}");
        }
        print!("{}", chunk.text);
        if !chunk.text.ends_with('\n') {
            println!();
        }
    }
}

pub fn print_json(source: &ExportedSource) -> Result<()> {
    println!("{}", serde_json::to_string_pretty(source)?);
    Ok(())
}