rta-core 0.3.0

Core analyzers and report model for the Rust Technical Audit Toolkit
Documentation
use crate::model::{AuditReport, RiskFinding};

pub fn render_evidence_json(report: &AuditReport) -> String {
    format!(
        concat!(
            "{{\n",
            "  \"schema_version\": \"rta.evidence.v1\",\n",
            "  \"repository_path\": {},\n",
            "  \"scores\": {{\n",
            "    \"overall\": {},\n",
            "    \"dependency_health\": {},\n",
            "    \"code_quality\": {},\n",
            "    \"architecture\": {},\n",
            "    \"testing\": {},\n",
            "    \"risk_posture\": {}\n",
            "  }},\n",
            "  \"evidence\": {{\n",
            "    \"overview\": {{\n",
            "      \"crate_count\": {},\n",
            "      \"package_count\": {},\n",
            "      \"workspace_members\": {},\n",
            "      \"cargo_configs\": {},\n",
            "      \"total_files\": {},\n",
            "      \"total_bytes\": {}\n",
            "    }},\n",
            "    \"dependencies\": {{\n",
            "      \"direct_dependencies\": {},\n",
            "      \"critical_dependencies\": {},\n",
            "      \"maintenance_risks\": {},\n",
            "      \"outdated_indicators\": {}\n",
            "    }},\n",
            "    \"architecture\": {{\n",
            "      \"style\": {},\n",
            "      \"separation_of_concerns\": {},\n",
            "      \"detected_layers\": {},\n",
            "      \"domain_boundaries\": {},\n",
            "      \"module_centralization_risks\": {},\n",
            "      \"circular_dependencies\": {}\n",
            "    }},\n",
            "    \"code_quality\": {{\n",
            "      \"lines_of_rust_code\": {},\n",
            "      \"rust_modules\": {},\n",
            "      \"function_count\": {},\n",
            "      \"average_function_size\": {:.1},\n",
            "      \"complexity_indicators\": {},\n",
            "      \"large_modules\": {},\n",
            "      \"god_module_candidates\": {}\n",
            "    }},\n",
            "    \"testing\": {{\n",
            "      \"has_tests\": {},\n",
            "      \"unit_test_files\": {},\n",
            "      \"integration_test_files\": {},\n",
            "      \"test_function_count\": {},\n",
            "      \"testing_structure\": {}\n",
            "    }}\n",
            "  }}\n",
            "}}\n"
        ),
        quoted(&report.repository_path),
        report.overall_score,
        report.dependencies.score,
        report.code_quality.score,
        report.architecture.score,
        report.testing.score,
        report.risks.score,
        report.overview.crate_count,
        report.overview.package_count,
        string_array(&report.overview.workspace_members),
        string_array(&report.overview.cargo_configs),
        report.overview.total_files,
        report.overview.total_bytes,
        report.dependencies.direct_dependencies,
        string_array(&report.dependencies.critical_dependencies),
        string_array(&report.dependencies.maintenance_risks),
        string_array(&report.dependencies.outdated_indicators),
        quoted(&report.architecture.architecture_style),
        quoted(&report.architecture.separation_of_concerns),
        string_array(&report.architecture.detected_layers),
        string_array(&report.architecture.domain_boundaries),
        string_array(&report.architecture.module_centralization_risks),
        string_array(&report.architecture.circular_dependencies),
        report.code_quality.lines_of_code,
        report.code_quality.module_count,
        report.code_quality.function_count,
        report.code_quality.average_function_size,
        string_array(&report.code_quality.complexity_indicators),
        string_array(&report.code_quality.large_modules),
        string_array(&report.code_quality.god_module_candidates),
        report.testing.has_tests,
        report.testing.unit_test_files,
        report.testing.integration_test_files,
        report.testing.test_function_count,
        quoted(&report.testing.testing_structure)
    )
}

pub fn render_risk_register_json(report: &AuditReport) -> String {
    let findings = report
        .risks
        .findings
        .iter()
        .enumerate()
        .map(|(idx, finding)| risk_register_finding_json(idx + 1, finding))
        .collect::<Vec<_>>()
        .join(",\n");

    format!(
        concat!(
            "{{\n",
            "  \"schema_version\": \"rta.risk-register.v1\",\n",
            "  \"repository_path\": {},\n",
            "  \"risk_score\": {},\n",
            "  \"finding_count\": {},\n",
            "  \"findings\": [\n",
            "{}\n",
            "  ]\n",
            "}}\n"
        ),
        quoted(&report.repository_path),
        report.risks.score,
        report.risks.findings.len(),
        findings
    )
}

pub fn render_review_questions_markdown(report: &AuditReport) -> String {
    let score_questions = score_review_questions(report);
    let finding_questions = finding_review_questions(report);

    let mut out = String::new();
    out.push_str("# Review Questions\n\n");
    out.push_str("## Score-Driven Questions\n\n");
    push_questions(&mut out, &score_questions);
    out.push('\n');
    out.push_str("## Finding-Driven Questions\n\n");
    push_questions(&mut out, &finding_questions);
    out
}

pub fn render_methodology_markdown() -> String {
    concat!(
        "# Methodology\n\n",
        "Rust Technical Audit Toolkit produces a heuristic technical due diligence snapshot from local repository evidence.\n\n",
        "## Scope\n\n",
        "- Repository structure, Cargo manifests, Rust source files, dependency declarations, test structure, and generated risk findings.\n",
        "- No AI analysis, network calls, package registry lookups, or external scanners are used by the evidence pack command.\n\n",
        "## Scoring Model\n\n",
        "| Area | Weight |\n",
        "| --- | ---: |\n",
        "| Dependency Health | 20% |\n",
        "| Code Quality | 25% |\n",
        "| Architecture | 25% |\n",
        "| Testing | 15% |\n",
        "| Risk Posture | 15% |\n\n",
        "Code Quality normalizes large modules against the repository's own p90 module size and penalizes hotspot density instead of absolute hotspot count.\n",
        "Architecture uses workspace crate count, average module depth, top-level `use crate::` fan-out, module centralization, and real top-level cycle detection.\n",
        "Dependency Health uses direct dependency surface, critical dependency signals, broad declarations, non-registry dependencies, and pre-1.0 API indicators.\n",
        "Testing rewards visible unit and integration structures. Risk Posture discounts material findings surfaced from the other analyzers.\n\n",
        "Bands: 90-100 excellent, 80-89 strong, 65-79 workable with focused review, 50-64 material diligence concerns, below 50 deep review or remediation required.\n\n",
        "## Interpretation\n\n",
        "Scores are triage indicators for senior engineering review, not absolute judgments. Evidence files preserve the signals used to produce the report so reviewers can validate, challenge, and deepen the assessment.\n"
    )
    .to_string()
}

fn risk_register_finding_json(index: usize, finding: &RiskFinding) -> String {
    format!(
        concat!(
            "    {{\n",
            "      \"id\": \"RISK-{:03}\",\n",
            "      \"severity\": {},\n",
            "      \"title\": {},\n",
            "      \"evidence\": {},\n",
            "      \"recommendation\": {}\n",
            "    }}"
        ),
        index,
        quoted(finding.severity.as_str()),
        quoted(&finding.title),
        quoted(&finding.evidence),
        quoted(&finding.recommendation)
    )
}

fn score_review_questions(report: &AuditReport) -> Vec<String> {
    let mut questions = Vec::new();
    if report.dependencies.score < 80 {
        questions.push(format!(
            "Dependency Health scored {}/100. Which critical dependencies lack explicit ownership, upgrade cadence, or replacement options?",
            report.dependencies.score
        ));
    }
    if report.code_quality.score < 80 {
        questions.push(format!(
            "Code Quality scored {}/100. Which large modules, branching hotspots, or God module candidates should be decomposed first?",
            report.code_quality.score
        ));
    }
    if report.architecture.score < 80 {
        questions.push(format!(
            "Architecture scored {}/100. Which boundaries are intended, documented, and enforced by crate or module ownership?",
            report.architecture.score
        ));
    }
    if report.testing.score < 80 {
        questions.push(format!(
            "Testing scored {}/100. Which critical business flows need regression or integration coverage before investment?",
            report.testing.score
        ));
    }
    if report.risks.score < 90 {
        questions.push(format!(
            "Risk Posture scored {}/100. Which risks should block, condition, or re-price the diligence decision?",
            report.risks.score
        ));
    }

    if questions.is_empty() {
        questions.push(
            "Which assumptions behind the high scores should be manually verified before relying on the audit?".into(),
        );
    }

    questions
}

fn finding_review_questions(report: &AuditReport) -> Vec<String> {
    if report.risks.findings.is_empty() {
        return vec![
            "No material findings were generated. Which risks are outside the current rule set and require manual review?".into(),
        ];
    }

    report
        .risks
        .findings
        .iter()
        .map(|finding| {
            format!(
                "{} finding `{}`: what evidence would confirm severity, ownership, and remediation cost?",
                finding.severity.as_str(),
                finding.title
            )
        })
        .collect()
}

fn push_questions(out: &mut String, questions: &[String]) {
    for question in questions {
        out.push_str(&format!("- {question}\n"));
    }
}

fn string_array(values: &[String]) -> String {
    let values = values
        .iter()
        .map(|value| quoted(value))
        .collect::<Vec<_>>()
        .join(",");
    format!("[{values}]")
}

fn quoted(value: &str) -> String {
    let mut out = String::from("\"");
    for ch in value.chars() {
        match ch {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            ch if ch.is_control() => out.push(' '),
            ch => out.push(ch),
        }
    }
    out.push('"');
    out
}