ccd-cli 1.0.0-alpha.9

Bootstrap and validate Continuous Context Development repositories
use serde::Serialize;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CommandAccess {
    ReadOnly,
    Mutating,
    Conditional,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum StateWriteMode {
    Never,
    Always,
    WhenFlagSet,
    WhenFlagUnset,
    ImplicitRuntimeDecision,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ControlFlagEffect {
    ApplyChanges,
    PreviewOnly,
    ConfirmWrite,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct ControlFlagDescriptor {
    pub(crate) name: &'static str,
    pub(crate) effect: ControlFlagEffect,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct CommandBehavior {
    pub(crate) access: CommandAccess,
    pub(crate) state_write_mode: StateWriteMode,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) control_flag: Option<ControlFlagDescriptor>,
    #[serde(skip_serializing_if = "is_false")]
    pub(crate) supports_fields: bool,
    #[serde(skip_serializing_if = "is_false")]
    pub(crate) compact_mcp_default: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) mcp_tool: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) mcp_command: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) note: Option<&'static str>,
}

const fn is_false(value: &bool) -> bool {
    !*value
}

impl CommandBehavior {
    const fn new(access: CommandAccess, state_write_mode: StateWriteMode) -> Self {
        Self {
            access,
            state_write_mode,
            control_flag: None,
            supports_fields: false,
            compact_mcp_default: false,
            mcp_tool: None,
            mcp_command: None,
            note: None,
        }
    }

    const fn with_control_flag(mut self, name: &'static str, effect: ControlFlagEffect) -> Self {
        self.control_flag = Some(ControlFlagDescriptor { name, effect });
        self
    }

    const fn with_fields(mut self) -> Self {
        self.supports_fields = true;
        self
    }

    const fn with_compact_mcp_default(mut self) -> Self {
        self.compact_mcp_default = true;
        self
    }

    const fn with_mcp(mut self, tool: &'static str, command: &'static str) -> Self {
        self.mcp_tool = Some(tool);
        self.mcp_command = Some(command);
        self
    }

    const fn with_note(mut self, note: &'static str) -> Self {
        self.note = Some(note);
        self
    }
}

const fn read_only() -> CommandBehavior {
    CommandBehavior::new(CommandAccess::ReadOnly, StateWriteMode::Never)
}

const fn mutating_always() -> CommandBehavior {
    CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::Always)
}

const fn preview_by_default(flag: &'static str) -> CommandBehavior {
    CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::WhenFlagSet)
        .with_control_flag(flag, ControlFlagEffect::ApplyChanges)
}

const fn confirm_before_write(flag: &'static str) -> CommandBehavior {
    CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::WhenFlagSet)
        .with_control_flag(flag, ControlFlagEffect::ConfirmWrite)
}

const fn writes_unless_preview_flag(flag: &'static str) -> CommandBehavior {
    CommandBehavior::new(CommandAccess::Mutating, StateWriteMode::WhenFlagUnset)
        .with_control_flag(flag, ControlFlagEffect::PreviewOnly)
}

const fn conditional_write(flag: &'static str) -> CommandBehavior {
    CommandBehavior::new(CommandAccess::Conditional, StateWriteMode::WhenFlagSet)
        .with_control_flag(flag, ControlFlagEffect::ApplyChanges)
}

const fn runtime_conditional() -> CommandBehavior {
    CommandBehavior::new(
        CommandAccess::Conditional,
        StateWriteMode::ImplicitRuntimeDecision,
    )
}

pub(crate) fn for_command_path(path: &[&str]) -> Option<CommandBehavior> {
    match path {
        ["describe"] => Some(read_only()),
        ["attach"] => Some(
            mutating_always()
                .with_mcp("ccd_repo", "attach")
                .with_note("bootstraps profile, marker, project overlay, and workspace-local state"),
        ),
        ["init"] => Some(
            mutating_always().with_note(
                "bootstraps the workspace and optionally scaffolds repo-local host integration assets without applying host-global mutations",
            ),
        ),
        ["scaffold"] => Some(
            mutating_always()
                .with_mcp("ccd_repo", "scaffold")
                .with_note("writes starter project-truth files; existing files require --force to overwrite"),
        ),
        ["host", "install"] => Some(
            mutating_always().with_note(
                "applies the selected repo-local host integration to the concrete host path; use --force to replace drifted target files",
            ),
        ),
        ["repo", "status"] => Some(read_only()),
        ["repo", "list"] => Some(read_only()),
        ["repo", "relink"] => Some(
            mutating_always()
                .with_note("updates the current workspace marker and relinks it to an existing project ID"),
        ),
        ["repo", "merge"] => Some(
            confirm_before_write("force").with_note(
                "destructive repair: merges two project identities and removes the source registry entry",
            ),
        ),
        ["repo", "split"] => Some(
            mutating_always()
                .with_note("creates a new project ID for the current workspace and copies project-overlay state"),
        ),
        ["link"] => Some(
            mutating_always()
                .with_mcp("ccd_repo", "link")
                .with_note("links the current workspace to a project ID and updates registry metadata"),
        ),
        ["check"] => Some(read_only().with_mcp("ccd_health", "check")),
        ["preflight"] => Some(read_only().with_mcp("ccd_health", "preflight")),
        ["start"] => Some(
            conditional_write("refresh")
                .with_fields()
                .with_compact_mcp_default()
                .with_mcp("ccd_session", "start")
                .with_note("default start is read-only; --refresh updates workspace-local continuity and cached next-step context; --activate adds session telemetry for the one-call raw CLI startup path; use --fields or MCP compact defaults to trim machine-readable output"),
        ),
        ["status"] => Some(
            read_only()
                .with_fields()
                .with_note("use --fields with --output json to request only the status surfaces you need"),
        ),
        ["context-check"] => Some(
            read_only()
                .with_mcp("ccd_context", "context-check")
                .with_note("evaluates the mid-session refresh contract without mutating continuity, memory, or escalation state"),
        ),
        ["policy-check"] => Some(read_only().with_mcp("ccd_state", "policy-check")),
        ["gc"] => Some(mutating_always().with_mcp("ccd_repo", "gc")),
        ["unlink"] => Some(mutating_always().with_mcp("ccd_repo", "unlink")),
        ["sync"] => Some(
            writes_unless_preview_flag("check")
                .with_mcp("ccd_health", "sync")
                .with_note("default sync writes generated mirrors; --check is read-only"),
        ),
        ["doctor"] => Some(
            read_only()
                .with_fields()
                .with_mcp("ccd_health", "doctor")
                .with_note("use --severity or --fields with --output json to trim machine-readable doctor output"),
        ),
        ["consistency"] => Some(
            read_only().with_note(
                "runs the deterministic artifact-consistency audit across surfaced references, projection freshness, cross-artifact agreement, and memory coherence",
            ),
        ),
        ["drift"] => Some(read_only().with_mcp("ccd_health", "drift")),
        ["handoff", "refresh"] => Some(
            preview_by_default("write")
                .with_mcp("ccd_state", "handoff-refresh")
                .with_note("preview is default; --write persists the refreshed handoff/export"),
        ),
        ["handoff", "export"] => Some(read_only()),
        ["handoff", "write"] => Some(mutating_always()),
        ["remember"] => Some(
            writes_unless_preview_flag("dry-run")
                .with_note("default remember writes memory; --dry-run previews the candidate entry"),
        ),
        ["memory", "search"] => Some(
            read_only()
                .with_mcp("ccd_memory_recall", "memory-search")
                .with_note("provider recall stays additive and falls back explicitly to authored memory when external recall is unavailable"),
        ),
        ["memory", "describe"] => Some(
            read_only()
                .with_mcp("ccd_memory_recall", "memory-describe")
                .with_note("describe returns the normalized recall envelope for the selected provider-native id"),
        ),
        ["memory", "expand"] => Some(
            read_only()
                .with_mcp("ccd_memory_recall", "memory-expand")
                .with_note("expand returns adjacent or provider-suggested recall context for the selected provider-native id"),
        ),
        ["memory", "sync-status"] => Some(
            read_only()
                .with_mcp("ccd_memory_recall", "memory-sync-status")
                .with_note("sync-status probes the advisory ingest provider for live freshness and lag reporting"),
        ),
        ["memory", "source-map"] => Some(
            read_only()
                .with_mcp("ccd_memory_recall", "memory-source-map")
                .with_note("source-map exports the advisory ingest provider source coverage exposed to CCD"),
        ),
        ["memory", "evidence", "submit"] => Some(
            mutating_always()
                .with_mcp("ccd_memory_capture", "memory-evidence-submit")
                .with_note("submits a bounded evidence summary into workspace-local state.db without persisting raw transcript bodies"),
        ),
        ["memory", "compact"] => Some(
            preview_by_default("write")
                .with_mcp("ccd_memory", "memory-compact")
                .with_note("preview is default; --write applies compaction changes"),
        ),
        ["memory", "candidate", "admit"] => Some(
            preview_by_default("write")
                .with_mcp("ccd_memory", "memory-candidate-admit")
                .with_note("preview is default; --write stages the reviewed candidate as a higher-scope `promotion_candidate` entry"),
        ),
        ["memory", "candidate", "extract"] => Some(
            preview_by_default("write")
                .with_mcp("ccd_memory_capture", "memory-candidate-extract")
                .with_note("preview is default; --write admits bounded evidence into workspace or work-stream memory through the governed queue path"),
        ),
        ["memory", "promote"] => Some(
            preview_by_default("write")
                .with_mcp("ccd_memory", "memory-promote")
                .with_note("preview is default; --write applies the promotion"),
        ),
        ["host-hook"] => Some(
            runtime_conditional().with_note(
                "invokes a named host-adapter lifecycle hook; on-compaction-notice may run bounded context-check plus best-effort evidence capture",
            ),
        ),
        ["telemetry", "report"] => Some(
            read_only().with_note(
                "summarizes recorded host-loop payload telemetry plus the latest prompt-cost analysis for this workspace",
            ),
        ),
        ["runtime-state", "export"] => Some(
            read_only()
                .with_compact_mcp_default()
                .with_mcp("ccd_state", "runtime-state-export"),
        ),
        ["runtime-state", "child-bootstrap"] => {
            Some(read_only().with_mcp("ccd_delegation", "child-bootstrap"))
        }
        ["escalation-state", "set"] => {
            Some(mutating_always().with_mcp("ccd_escalation", "set-escalation"))
        }
        ["escalation-state", "clear"] => {
            Some(mutating_always().with_mcp("ccd_escalation", "clear-escalation"))
        }
        ["escalation-state", "list"] => {
            Some(read_only().with_mcp("ccd_escalation", "list-escalations"))
        }
        ["radar-state"] => Some(
            runtime_conditional()
                .with_mcp("ccd_state", "radar-state")
                .with_note("may update workspace-local planning state when wrap-up or continuity drift requires it"),
        ),
        ["checkpoint"] => Some(
            runtime_conditional()
                .with_mcp("ccd_state", "checkpoint")
                .with_note(
                    "lightweight checkpoint: skips full evaluation, candidate updates, workflow hints, and approval steps",
                ),
        ),
        ["recovery", "write"] => Some(mutating_always().with_mcp("ccd_recovery", "write-recovery")),
        ["session", "open"] => Some(mutating_always().with_mcp("ccd_session", "session-open")),
        ["session-state", "start"] => Some(
            mutating_always().with_mcp("ccd_session_lifecycle", "start-session"),
        ),
        ["session-state", "heartbeat"] => Some(
            mutating_always()
                .with_mcp("ccd_session_lifecycle", "heartbeat-session")
                .with_note("renews an autonomous workspace-local session lease for the current actor"),
        ),
        ["session-state", "clear"] => Some(
            mutating_always().with_mcp("ccd_session_lifecycle", "clear-session"),
        ),
        ["session-state", "takeover"] => Some(
            mutating_always()
                .with_mcp("ccd_session_lifecycle", "takeover-session")
                .with_note("adopts a stale autonomous workspace-local session for a different actor"),
        ),
        ["session-state", "gates", "list"] => {
            Some(read_only().with_mcp("ccd_session_gates", "list-gates"))
        }
        ["session-state", "gates", "replace"] => {
            Some(mutating_always().with_mcp("ccd_session_gates", "replace-gates"))
        }
        ["session-state", "gates", "seed"] => {
            Some(mutating_always().with_mcp("ccd_session_gates", "seed-gates"))
        }
        ["session-state", "gates", "set-status"] => Some(
            mutating_always().with_mcp("ccd_session_gates", "set-gate-status"),
        ),
        ["session-state", "gates", "advance"] => {
            Some(mutating_always().with_mcp("ccd_session_gates", "advance-gates"))
        }
        ["session-state", "gates", "clear"] => {
            Some(mutating_always().with_mcp("ccd_session_gates", "clear-gates"))
        }
        ["hooks", "install"] => Some(mutating_always().with_mcp("ccd_health", "hooks-install")),
        ["hooks", "check"] => Some(read_only().with_mcp("ccd_health", "hooks-check")),
        ["skills", "install"] => Some(mutating_always().with_mcp("ccd_repo", "skills-install")),
        ["pod", "init"] => Some(mutating_always()),
        ["pod", "list"] => Some(read_only()),
        ["pod", "status"] => Some(read_only()),
        ["pod", "migrate-defaults"] => Some(
            preview_by_default("write").with_note(
                "preview is default; --write moves explicitly selected or suggested profile defaults into pod memory and policy",
            ),
        ),
        ["backlog", "pull"] => Some(mutating_always()),
        ["backlog", "scope"] => Some(mutating_always()),
        ["backlog", "next"] => Some(read_only()),
        ["backlog", "claim"] => Some(
            preview_by_default("write")
                .with_note("preview is default; --write executes the active backlog adapter mutation"),
        ),
        ["backlog", "set-status"] => Some(
            preview_by_default("write")
                .with_note("preview is default; --write executes the active backlog adapter mutation"),
        ),
        ["backlog", "complete"] => Some(
            preview_by_default("write")
                .with_note("preview is default; --write executes the active backlog adapter mutation"),
        ),
        ["backlog", "adapters"] => Some(read_only()),
        ["backlog", "promote-next"] => Some(
            preview_by_default("write")
                .with_note("preview is default; --write applies promotion through the active adapter"),
        ),
        ["backlog", "bootstrap-github"] => Some(mutating_always().with_mcp(
            "ccd_backlog",
            "backlog-bootstrap-github",
        )),
        ["backlog", "pull-github"] => Some(
            mutating_always().with_mcp("ccd_backlog", "backlog-pull-github"),
        ),
        ["backlog", "lint"] => Some(read_only().with_mcp("ccd_backlog", "backlog-lint")),
        ["backlog", "groom"] => Some(read_only().with_mcp("ccd_backlog", "backlog-groom")),
        ["codemap", "import"] => Some(mutating_always()),
        ["codemap", "status"] => Some(read_only().with_mcp("ccd_codemap", "codemap-status")),
        ["codemap", "query"] => Some(read_only().with_mcp("ccd_codemap", "codemap-query")),
        _ => None,
    }
}