wb-query is the weaveback analysis and metadata tool. Most subcommands are read-only queries over the database; tag is the one maintenance operation, updating prose tags in place and rebuilding FTS afterward.

CLI

Generated from cli-spec/wb-query-cli.adoc.

// <<wb-query-cli>>=
mod cli_generated;
use cli_generated::{Cli, Commands, LspCommands};
use clap::Parser;
use std::path::PathBuf;
// @

Error Type

// <<wb-query-error>>=
use thiserror::Error;
use weaveback_tangle::WeavebackError;

#[derive(Debug, Error)]
enum Error {
    #[error("{0}")]
    Noweb(#[from] WeavebackError),
    #[error("{0}")]
    Io(#[from] std::io::Error),
    #[error("{0}")]
    Api(#[from] weaveback_api::query::ApiError),
    #[error("{0}")]
    Lookup(#[from] weaveback_api::lookup::LookupError),
    #[error("{0}")]
    Lint(String),
    #[error("{0}")]
    Coverage(#[from] weaveback_api::coverage::CoverageApiError),
}

impl From<weaveback_tangle::db::DbError> for Error {
    fn from(e: weaveback_tangle::db::DbError) -> Self {
        Error::Noweb(WeavebackError::Db(e))
    }
}
// @

Dispatch

// <<wb-query-dispatch>>=
fn default_pathsep() -> String {
    if cfg!(windows) { ";".to_string() } else { ":".to_string() }
}

fn build_eval_config(sigil: char, include: String, allow_env: bool) -> weaveback_macro::evaluator::EvalConfig {
    use weaveback_macro::evaluator::EvalConfig;
    let pathsep = default_pathsep();
    let include_paths: Vec<PathBuf> = include
        .split(&pathsep)
        .map(PathBuf::from)
        .collect();
    EvalConfig {
        sigil,
        include_paths,
        allow_env,
        ..Default::default()
    }
}

fn run_tag_only(
    config_path: &std::path::Path,
    backend_override: Option<String>,
    model_override: Option<String>,
    endpoint_override: Option<String>,
    batch_size_override: Option<usize>,
    db_path: PathBuf,
) -> Result<(), Error> {
    use weaveback_api::tag;
    use weaveback_api::tangle::{TangleCfg, TagsCfg};
    use weaveback_api::tangle::{default_tags_backend, default_tags_batch_size, default_tags_model};

    let toml_tags: Option<TagsCfg> = std::fs::read_to_string(config_path).ok()
        .and_then(|s| toml::from_str::<TangleCfg>(&s).ok())
        .and_then(|c| c.tags);

    let tag_cfg = tag::TagConfig {
        backend: backend_override
            .or_else(|| toml_tags.as_ref().map(|t| t.backend.clone()))
            .unwrap_or_else(default_tags_backend),
        model: model_override
            .or_else(|| toml_tags.as_ref().map(|t| t.model.clone()))
            .unwrap_or_else(default_tags_model),
        endpoint: endpoint_override
            .or_else(|| toml_tags.as_ref().and_then(|t| t.endpoint.clone())),
        batch_size: batch_size_override
            .or_else(|| toml_tags.as_ref().map(|t| t.batch_size))
            .unwrap_or_else(default_tags_batch_size),
    };

    if !db_path.exists() {
        return Err(Error::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("Database not found at {}. Run wb-tangle first.", db_path.display()),
        )));
    }

    match weaveback_tangle::db::WeavebackDb::open(&db_path) {
        Ok(mut db) => {
            tag::run_auto_tag(&mut db, &tag_cfg);
            if let Err(e) = db.rebuild_prose_fts(None) {
                eprintln!("warning: FTS index rebuild failed: {e}");
            }
        }
        Err(e) => return Err(Error::Noweb(WeavebackError::Db(e))),
    }
    Ok(())
}

fn run_lsp(
    cmd: LspCommands,
    db_path: PathBuf,
    gen_dir: PathBuf,
    eval_config: weaveback_macro::evaluator::EvalConfig,
    override_cmd: Option<String>,
    override_lang: Option<String>,
) -> Result<(), Error> {
    let api_cmd = match cmd {
        LspCommands::Definition { out_file, line, col } =>
            weaveback_api::lsp_runner::LspCmd::Definition { out_file, line, col },
        LspCommands::References { out_file, line, col } =>
            weaveback_api::lsp_runner::LspCmd::References { out_file, line, col },
    };
    weaveback_api::lsp_runner::run_lsp(api_cmd, db_path, gen_dir, eval_config, override_cmd, override_lang)
        .map_err(Error::Io)
}

fn run(cli: Cli) -> Result<(), Error> {
    use weaveback_core::PathResolver;
    use weaveback_tangle::db::WeavebackDb;

    match cli.command {
        Commands::Where { out_file, line } => {
            let db = WeavebackDb::open_read_only(&cli.db)?;
            let resolver = PathResolver::new(PathBuf::from("."), cli.gen_dir);
            match weaveback_api::lookup::perform_where(&out_file, line, &db, &resolver)? {
                Some(v) => println!("{}", serde_json::to_string_pretty(&v).unwrap()),
                None    => println!("null"),
            }
        }

        Commands::Trace { out_file, line, col, sigil, include, allow_env } => {
            let eval_config = build_eval_config(sigil, include, allow_env);
            let db = WeavebackDb::open_read_only(&cli.db)?;
            let resolver = PathResolver::new(PathBuf::from("."), cli.gen_dir);
            match weaveback_api::lookup::perform_trace(&out_file, line, col, &db, &resolver, eval_config)? {
                Some(v) => println!("{}", serde_json::to_string_pretty(&v).unwrap()),
                None    => println!("null"),
            }
        }

        Commands::Impact { chunk } => {
            let v = weaveback_api::query::impact_analysis(&chunk, &cli.db)?;
            println!("{}", serde_json::to_string_pretty(&v).unwrap());
        }

        Commands::Graph { chunk } => {
            let dot = weaveback_api::query::chunk_graph_dot(chunk.as_deref(), &cli.db)?;
            print!("{dot}");
        }

        Commands::Tag { config, backend, model, endpoint, batch_size } => {
            run_tag_only(&config, backend, model, endpoint, batch_size, cli.db)?;
        }

        Commands::Tags { file } => {
            let blocks = weaveback_api::query::list_block_tags(file.as_deref(), &cli.db)?;
            if blocks.is_empty() {
                eprintln!("No tagged blocks found. Add a [tags] section to weaveback.toml and run wb-tangle.");
            } else {
                let block_values: Vec<serde_json::Value> = blocks.iter().map(|b| serde_json::json!({
                    "src_file": b.src_file,
                    "block_index": b.block_index,
                    "block_type": b.block_type,
                    "line_start": b.line_start,
                    "tags": b.tags,
                })).collect();
                let v = serde_json::json!({ "tagged_blocks": block_values });
                println!("{}", serde_json::to_string_pretty(&v).unwrap());
            }
        }

        Commands::Lint { paths, strict, rule, json } => {
            weaveback_api::lint::run_lint(paths, strict, rule, json)
                .map_err(Error::Lint)?;
        }

        Commands::Attribute { scan_stdin, summary, locations, sigil, include, allow_env } => {
            let eval_config = build_eval_config(sigil, include, allow_env);
            weaveback_api::coverage::run_attribute(
                scan_stdin, summary, locations, cli.db, cli.gen_dir, eval_config,
            )?;
        }

        Commands::Coverage { summary, top_sources, top_sections, explain_unattributed, lcov_file } => {
            weaveback_api::coverage::run_coverage(
                summary, top_sources, top_sections, explain_unattributed, lcov_file,
                cli.db, cli.gen_dir,
            )?;
        }

        Commands::Cargo { diagnostics_only, args, sigil, include, allow_env } => {
            let eval_config = build_eval_config(sigil, include, allow_env);
            weaveback_api::coverage::run_cargo_annotated(
                args, diagnostics_only, cli.db, cli.gen_dir, eval_config,
            )?;
        }

        Commands::Lsp { lsp_cmd, lsp_lang, sigil, include, allow_env, cmd } => {
            let eval_config = build_eval_config(sigil, include, allow_env);
            run_lsp(cmd, cli.db, cli.gen_dir, eval_config, lsp_cmd, lsp_lang)?;
        }

        Commands::Search { query, limit } => {
            weaveback_api::coverage::run_search(query, limit, cli.db)?;
        }
    }
    Ok(())
}

fn main() {
    let cli = Cli::parse();
    if let Err(e) = run(cli) {
        eprintln!("wb-query: {e}");
        std::process::exit(1);
    }
}
// @

Tests

// <<wb-query-tests>>=
#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    struct TestWorkspace {
        root: PathBuf,
    }

    impl TestWorkspace {
        fn new() -> Self {
            let unique = format!(
                "wb-query-tests-{}-{}",
                std::process::id(),
                std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap()
                    .as_nanos()
            );
            let root = std::env::temp_dir().join(unique);
            std::fs::create_dir_all(&root).unwrap();
            Self { root }
        }

        fn db(&self) -> PathBuf {
            self.root.join("weaveback.db")
        }

        fn gen_dir(&self) -> PathBuf {
            self.root.join("gen")
        }

        fn open_db(&mut self) -> weaveback_tangle::db::WeavebackDb {
            weaveback_tangle::db::WeavebackDb::open(self.db()).unwrap()
        }
    }

    impl Drop for TestWorkspace {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.root);
        }
    }

    #[test]
    fn test_build_eval_config() {
        let cfg = super::build_eval_config('%', "a:b".to_string(), true);
        assert_eq!(cfg.sigil, '%');
        assert_eq!(cfg.include_paths.len(), 2);
        assert!(cfg.allow_env);
    }

    #[test]
    fn run_where_missing_db() {
        let ws = TestWorkspace::new();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Where {
                out_file: "test.rs".to_string(),
                line: 1,
            },
        };
        let res = run(cli);
        assert!(res.is_err());
        assert!(res.unwrap_err().to_string().contains("weaveback.db"));
    }

    #[test]
    fn run_where_success() {
        let mut ws = TestWorkspace::new();
        let mut db = ws.open_db();
        db.set_chunk_defs(&[weaveback_tangle::db::ChunkDefEntry {
            src_file: "test.adoc".to_string(),
            chunk_name: "test".to_string(),
            nth: 0,
            def_start: 1,
            def_end: 10,
        }]).unwrap();
        db.set_baseline("test.rs", b"content").unwrap();
        // Seed some attribution data if necessary, but perform_where mostly uses baseline/chunks.

        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Where {
                out_file: "test.rs".to_string(),
                line: 1,
            },
        };
        // This won't find much without proper attribution, but it exercises the run() dispatch.
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_impact_success() {
        let mut ws = TestWorkspace::new();
        ws.open_db();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Impact {
                chunk: "test".to_string(),
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_graph_success() {
        let mut ws = TestWorkspace::new();
        ws.open_db();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Graph {
                chunk: None,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_tags_success() {
        let mut ws = TestWorkspace::new();
        ws.open_db();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Tags {
                file: None,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_search_success() {
        let mut ws = TestWorkspace::new();
        let mut db = ws.open_db();
        db.rebuild_prose_fts(None).unwrap();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Search {
                query: "test".to_string(),
                limit: 10,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_tag_success() {
        let mut ws = TestWorkspace::new();
        ws.open_db();
        let config_path = ws.root.join("weaveback.toml");
        std::fs::write(&config_path, "[tags]\nbackend=\"ollama\"\n").unwrap();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Tag {
                config: config_path,
                backend: None,
                model: None,
                endpoint: None,
                batch_size: None,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_lint_success() {
        let ws = TestWorkspace::new();
        let adoc = ws.root.join("test.adoc");
        std::fs::write(&adoc, "= Test\n\n[source,rust]\n----\n// <<@file test.rs>>=\nfn main() {}\n// @\n----\n").unwrap();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Lint {
                paths: vec![ws.root.clone()],
                strict: false,
                rule: None,
                json: false,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_attribute_success() {
        let mut ws = TestWorkspace::new();
        ws.open_db();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Attribute {
                scan_stdin: false,
                summary: false,
                locations: vec!["test.rs:1".to_string()],
                sigil: '%',
                include: ".".to_string(),
                allow_env: false,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }

    #[test]
    fn run_coverage_success() {
        let mut ws = TestWorkspace::new();
        ws.open_db();
        let lcov = ws.root.join("lcov.info");
        std::fs::write(&lcov, "SF:test.rs\nDA:1,1\nend_of_record\n").unwrap();
        let cli = Cli {
            db: ws.db(),
            gen_dir: ws.gen_dir(),
            command: Commands::Coverage {
                summary: true,
                top_sources: 10,
                top_sections: 3,
                explain_unattributed: false,
                lcov_file: lcov,
            },
        };
        let res = run(cli);
        assert!(res.is_ok());
    }
}
// @

Assembly

// <<@file wb-query/src/main.rs>>=
// <<wb-query-cli>>
// <<wb-query-error>>
// <<wb-query-dispatch>>
// <<wb-query-tests>>
// @