cleanlib-cli 0.1.1

Terminal interface to CleanLibrary — query dependency verdicts and scan package manifests for ALLOW / DENY / WARN signals from the terminal or CI pipelines.
//! Customer-facing sanitization helpers — CLEANLIB-128 / Jira CLEANLIB-31a.
//!
//! Root rule: `[[feedback_no_internal_codenames_in_ui]]` (Priority-1) — no
//! internal engine-tag identifiers (VECTOR_*, DM_*, llm_*, mock_*,
//! fixture_*, stub_*, test_*) ever surface at a customer-facing render
//! boundary.
//!
//! Sister of the cleanlib-vscode-extension `src/sanitize.ts::maskEngineTag()`
//! shipped in cycle-8 §3.2 (c) for `llm_*` → "AI-derived" and extended in
//! cycle-10 CLEANLIB-128 (PR #19 / merge `b22dd74`) for `VECTOR_*` →
//! "Engine signal" + `DM_*` → "Policy decision". This file ports the same
//! pattern to the Rust CLI render path so the hover (extension) and
//! `cleanlib verdict` (CLI) render shapes stay symmetric.

/// Mask an engine-tag identifier into customer-facing language. Returns the
/// input unchanged when no codename pattern matches (pass-through is safe).
///
/// Patterns covered (cycle-10 close):
/// - `^VECTOR_.*$`      → "Engine signal"  (e.g., VECTOR_VERDICT, VECTOR_EVENT)
/// - `^DM_.*$`          → "Policy decision" (e.g., DM_THRESHOLD_BLOCK)
/// - `^llm_.*$` (case-insensitive) → "AI-derived" (cycle-8 §3.2 (c) precedent)
///
/// Non-codename inputs (`ALLOW`, `manual`, `policy-engine-v1`, etc.) pass
/// through unchanged. Empty / whitespace inputs pass through unchanged.
pub fn mask_engine_tag(s: &str) -> String {
    // Cycle-8 §3.2 (c) precedent: llm_* → "AI-derived"
    // (case-insensitive per the TS sister regex `/^llm_[a-z0-9_-]+$/i`).
    if let Some(rest) = strip_case_insensitive_prefix(s, "llm_") {
        // Validate the rest is identifier-shaped (alnum / `_` / `-`).
        if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
            return "AI-derived".to_string();
        }
    }

    // Cycle-10 CLEANLIB-128: VECTOR_* → "Engine signal"
    if let Some(rest) = s.strip_prefix("VECTOR_") {
        if !rest.is_empty() {
            return "Engine signal".to_string();
        }
    }

    // Cycle-10 CLEANLIB-128: DM_* → "Policy decision"
    if let Some(rest) = s.strip_prefix("DM_") {
        if !rest.is_empty() {
            return "Policy decision".to_string();
        }
    }

    s.to_string()
}

/// Hide internal mock/fixture/stub/test labels entirely from customer
/// render — sister of `cleanlib-vscode-extension::maskFixtureLabel`
/// authored for CLEANLIB-21 "internal mock text exposed" sub-case.
/// Returns the empty string to signal "do not render this field at all";
/// callers must check `is_empty()` and skip the line.
pub fn mask_fixture_label(s: &str) -> String {
    let lower = s.to_ascii_lowercase();
    if lower.starts_with("mock_")
        || lower.starts_with("fixture_")
        || lower.starts_with("stub_")
        || lower.starts_with("test_")
        || lower == "mock"
        || lower == "fixture"
        || lower == "stub"
    {
        return String::new();
    }
    s.to_string()
}

/// Helper: case-insensitive `strip_prefix` (std::str::strip_prefix is
/// case-sensitive). Returns `Some(rest)` when `s` starts with `prefix`
/// regardless of case.
fn strip_case_insensitive_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
    if s.len() < prefix.len() {
        return None;
    }
    let head = &s[..prefix.len()];
    if head.eq_ignore_ascii_case(prefix) {
        Some(&s[prefix.len()..])
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // CLEANLIB-128 / Jira CLEANLIB-31a — VECTOR_* mask.

    #[test]
    fn vector_verdict_masks_to_engine_signal() {
        assert_eq!(mask_engine_tag("VECTOR_VERDICT"), "Engine signal");
    }

    #[test]
    fn vector_event_masks_to_engine_signal() {
        assert_eq!(mask_engine_tag("VECTOR_EVENT"), "Engine signal");
    }

    #[test]
    fn vector_arbitrary_suffix_masks_to_engine_signal() {
        assert_eq!(mask_engine_tag("VECTOR_NEW_LABEL_2026"), "Engine signal");
    }

    // CLEANLIB-128 / Jira CLEANLIB-31a — DM_* mask.

    #[test]
    fn dm_threshold_block_masks_to_policy_decision() {
        assert_eq!(mask_engine_tag("DM_THRESHOLD_BLOCK"), "Policy decision");
    }

    #[test]
    fn dm_arbitrary_suffix_masks_to_policy_decision() {
        assert_eq!(mask_engine_tag("DM_FUTURE_RULE"), "Policy decision");
    }

    // Cycle-8 §3.2 (c) precedent — llm_* mask (case-insensitive).

    #[test]
    fn llm_anthropic_masks_to_ai_derived() {
        assert_eq!(mask_engine_tag("llm_anthropic"), "AI-derived");
        assert_eq!(mask_engine_tag("LLM_OPENAI"), "AI-derived");
        assert_eq!(mask_engine_tag("Llm_Mistral-7b"), "AI-derived");
    }

    // Pass-through cases.

    #[test]
    fn allow_passes_through_unchanged() {
        assert_eq!(mask_engine_tag("ALLOW"), "ALLOW");
        assert_eq!(mask_engine_tag("DENY"), "DENY");
        assert_eq!(mask_engine_tag("WARN"), "WARN");
        assert_eq!(mask_engine_tag("ALLOWED_NO_FINDINGS"), "ALLOWED_NO_FINDINGS");
        assert_eq!(mask_engine_tag("INSUFFICIENT_DATA"), "INSUFFICIENT_DATA");
    }

    #[test]
    fn unrelated_labels_pass_through() {
        assert_eq!(mask_engine_tag("manual"), "manual");
        assert_eq!(mask_engine_tag("policy-engine-v1"), "policy-engine-v1");
        assert_eq!(mask_engine_tag(""), "");
    }

    #[test]
    fn prefix_alone_does_not_mask() {
        // Bare prefix (no suffix) should not mask; otherwise we'd
        // accidentally swallow legitimate "VECTOR" / "DM" / "llm" symbols.
        assert_eq!(mask_engine_tag("VECTOR_"), "VECTOR_");
        assert_eq!(mask_engine_tag("DM_"), "DM_");
        assert_eq!(mask_engine_tag("llm_"), "llm_");
        assert_eq!(mask_engine_tag("VECTOR"), "VECTOR");
        assert_eq!(mask_engine_tag("DM"), "DM");
    }

    // mask_fixture_label coverage — CLEANLIB-21 "internal mock text" sub-case.

    #[test]
    fn mock_label_hides_entirely() {
        assert_eq!(mask_fixture_label("mock_npm_fixture"), "");
        assert_eq!(mask_fixture_label("MOCK_npm_fixture"), "");
        assert_eq!(mask_fixture_label("mock"), "");
        assert_eq!(mask_fixture_label("Mock"), "");
    }

    #[test]
    fn fixture_label_hides_entirely() {
        assert_eq!(mask_fixture_label("fixture_stale"), "");
        assert_eq!(mask_fixture_label("Fixture_corpus"), "");
        assert_eq!(mask_fixture_label("fixture"), "");
    }

    #[test]
    fn stub_label_hides_entirely() {
        assert_eq!(mask_fixture_label("stub_value"), "");
        assert_eq!(mask_fixture_label("stub"), "");
    }

    #[test]
    fn test_prefix_label_hides_entirely() {
        assert_eq!(mask_fixture_label("test_input"), "");
        assert_eq!(mask_fixture_label("TEST_seed"), "");
    }

    #[test]
    fn customer_facing_label_passes_through() {
        assert_eq!(mask_fixture_label("Cleanstart-curated catalog"), "Cleanstart-curated catalog");
        assert_eq!(mask_fixture_label("npm"), "npm");
        assert_eq!(mask_fixture_label("policy-decision"), "policy-decision");
        assert_eq!(mask_fixture_label(""), "");
    }
}