aiscope 0.1.1

DevTools for your AI coding tools' memory. See what Cursor, Claude Code, and Copilot actually remember about your project — and where they disagree.
//! Subcommand dispatch.

pub mod check;
pub mod scan;
pub mod watch;

use crate::detect::tokens;
use crate::model::{ContextBundle, Rule};
use crate::reason::ReasonMode;
use crate::scanner::ScanOptions;
use crate::{parse, reason, scanner};
use std::path::Path;

#[derive(Debug, Default, Clone, Copy)]
pub struct PipelineOptions {
    pub mode: ReasonMode,
    pub include_user: bool,
}

pub fn build_bundle(repo_root: &Path, opts: PipelineOptions) -> ContextBundle {
    let pairs = scanner::scan_all(
        repo_root,
        ScanOptions {
            include_user: opts.include_user,
        },
    );

    let mut sources = Vec::with_capacity(pairs.len());
    let mut statements = Vec::new();
    for (source, text) in pairs {
        let source_index = sources.len();
        sources.push(source);
        statements.extend(parse::parse(source_index, &text));
    }

    let mut assertions = Vec::new();
    for (i, stmt) in statements.iter().enumerate() {
        let canon = crate::canon::canonicalize(&stmt.text);
        assertions.extend(crate::extract::pattern::extract(i, stmt, &canon));
    }

    let mut conflicts = reason::detect_duplicates(&statements, &sources);
    conflicts.extend(reason::detect_clashes(
        &assertions,
        &statements,
        &sources,
        opts.mode,
    ));
    conflicts.extend(reason::detect_duplicate_names(&sources));
    conflicts.extend(reason::detect_agent_tool_mismatches(&sources, &statements));

    let mut rules: Vec<Rule> = statements
        .iter()
        .map(|s| Rule {
            source_index: s.source_index,
            text: s.text.clone(),
            tokens: 0,
            fingerprint: reason::fingerprint(&s.text),
        })
        .collect();
    tokens::rescore(&mut rules);

    let total_tokens: usize = rules.iter().map(|r| r.tokens).sum();
    let stale_tokens: usize = conflicts
        .iter()
        .filter(|c| matches!(c.kind, crate::model::ConflictKind::Duplicate))
        .map(|c| rules.get(c.right).map(|r| r.tokens).unwrap_or(0))
        .sum();

    ContextBundle {
        root: repo_root.to_path_buf(),
        sources,
        statements,
        assertions,
        rules,
        conflicts,
        total_tokens,
        stale_tokens,
    }
}