heddle-cli 0.15.5

An AI-native version control system
// SPDX-License-Identifier: Apache-2.0
//! Show command.

use anyhow::Result;
use repo::{Repository, format_confidence};
use verbs::status::next_action::canonical_git_import_ref_command;

// The wire payloads live in cli-contract so the schema registry registers
// the real serialization types.
pub use heddle_cli_contract::cli::commands::wire::history::{
    ShowAgentInfo as AgentInfo, ShowImportGuidanceOutput, ShowOutput,
    ShowPrincipalInfo as PrincipalInfo, ShowVerificationInfo as VerificationInfo,
};

use super::{
    action_line::{print_next_step, print_next_step_dim},
    history_target::{require_resolved_state, resolve_state_id},
    next_action::{NextActionValidationContext, normalized_action, write_full_command_json},
    snapshot::ensure_current_state,
    verification_health::{PlainGitVerificationProbe, build_plain_git_verification_probe},
};
use crate::{
    cli::{Cli, should_output_json, style},
    config::UserConfig,
};

pub fn cmd_show(cli: &Cli, state_spec: Option<String>) -> Result<()> {
    cmd_show_with_output_kind(cli, state_spec, "show")
}

fn cmd_show_with_output_kind(
    cli: &Cli,
    state_spec: Option<String>,
    output_kind: &'static str,
) -> Result<()> {
    let state_spec = state_spec.unwrap_or_else(|| "HEAD".to_string());
    let cwd = std::env::current_dir()?;
    let start = cli.repo.as_ref().unwrap_or(&cwd);
    if let Some(probe) = build_plain_git_verification_probe(start)? {
        return render_plain_git_show(cli, &probe, &state_spec, output_kind);
    }

    let repo = Repository::open(start)?;
    if repo.capability() == repo::RepositoryCapability::GitOverlay
        && repo.current_state()?.is_none()
    {
        if ingest::GitSource::open(repo.root())?
            .resolve_history_revision(&state_spec)
            .is_ok()
        {
            return render_unbound_overlay_show(cli, &repo, &state_spec, output_kind);
        }
        if matches!(state_spec.as_str(), "HEAD" | "@") {
            ensure_current_state(
                &repo,
                &UserConfig::load_default()?,
                Some("Bootstrap git-overlay before showing HEAD".to_string()),
            )?;
        }
    }
    let id = resolve_state_id(&repo, &state_spec)?;

    let state = require_resolved_state(&repo, &id)?;

    let output = ShowOutput {
        output_kind,
        repository_capability: repo.capability_label().to_string(),
        storage_model: repo.storage_model_label().to_string(),
        import_guidance: repo
            .git_import_guidance()?
            .map(|hint| ShowImportGuidanceOutput {
                current_branch: hint.current_branch,
                missing_branch_count: hint.missing_branch_count,
                missing_branches: hint.missing_branches,
                recommended_command: hint.recommended_command,
            }),
        state_id: state.state_id.short(),
        state_id_full: state.state_id.to_string_full(),
        content_hash: state.compute_hash().to_hex(),
        tree: state.tree.to_hex(),
        parents: state.parents.iter().map(|p| p.short()).collect(),
        intent: state.intent.clone(),
        confidence: state.confidence,
        principal: PrincipalInfo {
            name: state.attribution.principal.name_lossy().into_owned(),
            email: state.attribution.principal.email_lossy().into_owned(),
        },
        agent: state.attribution.agent.as_ref().map(|a| AgentInfo {
            provider: a.provider.clone(),
            model: a.model.clone(),
            session_id: a.session_id.clone(),
            policy_id: a.policy_id.clone(),
        }),
        created_at: state.created_at.to_rfc3339(),
        status: format!("{:?}", state.status),
        verification: state.verification.as_ref().map(|v| VerificationInfo {
            tests_passed: v.tests_passed,
            tests_failed: v.tests_failed,
            coverage_pct: v.coverage_pct,
            coverage_delta: v.coverage_delta,
            lint_warnings: v.lint_warnings,
        }),
        git_checkpoint: repo
            .latest_git_checkpoint_for_state(&state.state_id)
            .ok()
            .flatten()
            .map(|record| record.git_commit),
    };

    if should_output_json(cli, Some(repo.config())) {
        write_full_command_json(
            &output,
            NextActionValidationContext::without_repo(&["show"]),
        )?;
    } else {
        render_state(&output, cli.verbose > 0);
    }

    Ok(())
}

fn render_unbound_overlay_show(
    cli: &Cli,
    repo: &Repository,
    revision: &str,
    output_kind: &'static str,
) -> Result<()> {
    let projection = ingest::OverlayHistory::project_tip(repo.root(), revision)?;
    let git_oid = projection.git_oid;
    let state = projection.state;
    let output = ShowOutput {
        output_kind,
        repository_capability: repo.capability_label().to_string(),
        storage_model: repo.storage_model_label().to_string(),
        import_guidance: repo
            .git_import_guidance()?
            .map(|hint| ShowImportGuidanceOutput {
                current_branch: hint.current_branch,
                missing_branch_count: hint.missing_branch_count,
                missing_branches: hint.missing_branches,
                recommended_command: hint.recommended_command,
            }),
        state_id: state.state_id.short(),
        state_id_full: state.state_id.to_string_full(),
        content_hash: state.compute_hash().to_hex(),
        tree: state.tree.to_hex(),
        parents: projection
            .parent_ids
            .iter()
            .map(|parent| parent.short())
            .collect(),
        intent: state.intent.clone(),
        confidence: state.confidence,
        principal: PrincipalInfo {
            name: state.attribution.principal.name_lossy().into_owned(),
            email: state.attribution.principal.email_lossy().into_owned(),
        },
        agent: state.attribution.agent.as_ref().map(|agent| AgentInfo {
            provider: agent.provider.clone(),
            model: agent.model.clone(),
            session_id: agent.session_id.clone(),
            policy_id: agent.policy_id.clone(),
        }),
        created_at: state.created_at.to_rfc3339(),
        status: format!("{:?}", state.status),
        verification: state
            .verification
            .as_ref()
            .map(|verification| VerificationInfo {
                tests_passed: verification.tests_passed,
                tests_failed: verification.tests_failed,
                coverage_pct: verification.coverage_pct,
                coverage_delta: verification.coverage_delta,
                lint_warnings: verification.lint_warnings,
            }),
        git_checkpoint: Some(git_oid),
    };
    if should_output_json(cli, Some(repo.config())) {
        write_full_command_json(
            &output,
            NextActionValidationContext::without_repo(&["show"]),
        )?;
    } else {
        render_state(&output, cli.verbose > 0);
    }
    Ok(())
}

fn render_plain_git_show(
    cli: &Cli,
    probe: &PlainGitVerificationProbe,
    state_spec: &str,
    output_kind: &'static str,
) -> Result<()> {
    if should_output_json(cli, None) {
        let payload = serde_json::json!({
            "repository_capability": "plain-git",
            "output_kind": output_kind,
            "storage_model": "git",
            "requested": state_spec,
            "state": null,
            "verification": &probe.trust,
            "recommended_action": normalized_action(&probe.trust.recommended_action),
            "recovery_commands": &probe.trust.recovery_commands,
        });
        write_full_command_json(
            &payload,
            NextActionValidationContext::without_repo(&["show"]),
        )?;
    } else {
        println!("Git repo, Heddle not initialized");
        if let Some(branch) = &probe.git_branch {
            println!("Git branch: {}", style::bold(branch));
        }
        println!("State: unavailable until this Git repo is initialized and imported");
        if probe.trust.recommended_action.is_empty() {
            print_next_step("heddle init");
        } else {
            print_next_step(&probe.trust.recommended_action);
        }
        if let Some(branch) = &probe.git_branch
            && probe.trust.recommended_action != canonical_git_import_ref_command(branch)
        {
            println!(
                "Then: {}",
                style::bold(&canonical_git_import_ref_command(branch))
            );
        }
    }
    Ok(())
}

/// Short content-hash prefix labeled so it is not offered as a state spec.
fn labeled_content_hash_prefix(content_hash: &str) -> String {
    let prefix = match content_hash.get(..8) {
        Some(prefix) => prefix,
        None => content_hash,
    };
    format!("content_hash {prefix}")
}

fn render_state(output: &ShowOutput, verbose: bool) {
    // Mode preamble is read-path noise (heddle#275); show it only under
    // `-v`. `heddle status` covers it for the common case.
    let mut wrote_header = false;
    if verbose {
        println!(
            "Repository: {}",
            crate::cli::render::repository_mode_label(
                &output.repository_capability,
                &output.storage_model
            )
        );
        wrote_header = true;
    }
    if let Some(hint) = &output.import_guidance {
        println!(
            "{}",
            crate::cli::render::git_only_branch_summary(
                &hint.missing_branches,
                hint.missing_branch_count,
            )
        );
        print_next_step_dim(&hint.recommended_command);
        wrote_header = true;
    }
    // Only emit the spacer when a header preceded it; otherwise it would be
    // an orphaned leading blank line (heddle#275 r2).
    if wrote_header {
        println!();
    }
    // Identifiers are dimmed: structurally important but not the
    // editorial focus. The parenthetical is content_hash, not a
    // state spec — label it so `heddle show <paren>` is not offered.
    println!(
        "State: {} ({})",
        style::state_id(&output.state_id),
        style::dim(&labeled_content_hash_prefix(&output.content_hash))
    );
    println!("Full ID: {}", style::dim(&output.state_id_full));
    println!("Tree: {}", style::dim(&output.tree));

    if !output.parents.is_empty() {
        let dimmed: Vec<String> = output.parents.iter().map(|p| style::dim(p)).collect();
        println!("Parents: {}", dimmed.join(", "));
    } else {
        println!("Parents: {}", style::dim("(root state)"));
    }

    println!();

    if let Some(intent) = &output.intent {
        // Intent line carries the human-meaningful summary; bold it.
        println!("Intent: {}", style::bold(intent));
    }

    // Render `Confidence: —` for an absent value rather than skipping
    // the line. An unset confidence is meaningful — it tells the
    // reader the agent never asserted one — and silently omitting it
    // collapses that signal into "field missing". `format_confidence`
    // is the single source of truth for the absent sentinel; see
    // `repo::snapshot_metadata` for the rationale. We band the value
    // via `style::confidence` so high/mid/low/absent are all
    // distinguishable at a glance.
    let confidence_text = format_confidence(output.confidence);
    println!(
        "Confidence: {}",
        style::confidence(output.confidence, &confidence_text)
    );

    println!();
    println!(
        "Principal: {}",
        style::principal(&output.principal.name, &output.principal.email)
    );

    if let Some(agent) = &output.agent {
        println!(
            "Agent: {}",
            style::dim(&format!("{}/{}", agent.provider, agent.model))
        );
        if let Some(session) = &agent.session_id {
            println!("  Session: {}", style::dim(session));
        }
        if let Some(policy) = &agent.policy_id {
            println!("  Policy: {}", style::dim(policy));
        }
    }

    println!();
    println!("Timestamp: {}", style::dim(&output.created_at));
    println!("Status: {}", output.status);
    if let Some(git_checkpoint) = &output.git_checkpoint {
        println!(
            "Git checkpoint: {}",
            style::dim(&git_checkpoint[..std::cmp::min(12, git_checkpoint.len())])
        );
    } else if verbose {
        // "Capture durability: local only" is the default for any
        // non-checkpointed state — informative on demand, noise on the
        // default `heddle show` view (which already implies "this state
        // doesn't carry a Git checkpoint" by the absence of the line
        // above).
        println!("Capture durability: {}", style::dim("local only"));
    }

    if let Some(v) = &output.verification {
        println!();
        println!("Verification:");
        if let Some(passed) = v.tests_passed {
            println!("  Tests passed: {}", passed);
        }
        if let Some(failed) = v.tests_failed {
            println!("  Tests failed: {}", failed);
        }
        if let Some(coverage) = v.coverage_pct {
            println!("  Coverage: {:.1}%", coverage);
        }
        if let Some(delta) = v.coverage_delta {
            println!("  Coverage delta: {:+.1}%", delta);
        }
        if let Some(warnings) = v.lint_warnings {
            println!("  Lint warnings: {}", warnings);
        }
    }
}