harn-cli 0.8.86

CLI for the Harn programming language — run, test, REPL, format, and lint
use harn_serve::adapters::acp::{
    ACP_SCHEMA_COMPATIBILITY, HARN_AGENT_EVENT_KINDS, HARN_AGENT_EVENT_METHOD,
    HARN_CONTENT_EXTENSION_FIELDS, HARN_PROVIDER_CATALOG_METHOD, HARN_SESSION_UPDATE_EXTENSIONS,
    HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS,
};

use super::constants::*;
use super::support::*;
use super::values::*;

/// Emit the dependency-free Rust binding.
///
/// Unlike the Swift/Go/Python artifacts (which publish the full typed envelope
/// surface), the Rust artifact is intentionally a flat, allocation-free module
/// of `pub const` wire-name vocabulary plus the published slices. Burin Code
/// vendors it verbatim as `protocol/src/generated.rs` and routes/matches on the
/// constants instead of hand-maintaining a parallel method enum — the exact
/// HARN_BOUNDARY "don't mirror Harn wire vocabulary by hand" fix.
pub(super) fn generate_rust() -> String {
    let mut out = String::new();
    out.push_str("// GENERATED by `harn dump-protocol-artifacts` - do not edit by hand.\n");
    out.push_str("// Source: Harn adapter schemas and Rust wire vocabulary.\n\n");
    out.push_str(
        "//! Dependency-free Rust bindings for Harn's host/integrator protocol surface.\n",
    );
    out.push_str("//!\n");
    out.push_str("//! Mirrors the TypeScript, Swift, Python, and Go artifacts generated\n");
    out.push_str("//! alongside this module. Every value is the literal JSON wire string Harn\n");
    out.push_str("//! emits, so downstream Rust hosts can route and match on these constants\n");
    out.push_str("//! instead of hand-maintaining a parallel method list. Adding a wire value\n");
    out.push_str("//! is additive and minor-version compatible.\n");
    out.push_str("#![allow(dead_code)]\n\n");

    out.push_str("/// Harn release that generated this binding.\n");
    out.push_str(&format!(
        "pub const HARN_PROTOCOL_ARTIFACT_VERSION: &str = {};\n\n",
        json_string_literal(env!("CARGO_PKG_VERSION"))
    ));
    out.push_str("/// Upstream ACP schema version Harn tracks.\n");
    out.push_str(&format!(
        "pub const ACP_SCHEMA_COMPATIBILITY: &str = {};\n\n",
        json_string_literal(ACP_SCHEMA_COMPATIBILITY)
    ));
    out.push_str("/// JSON-RPC method for `_harn/agentEvent` extension notifications.\n");
    out.push_str(&format!(
        "pub const HARN_AGENT_EVENT_METHOD: &str = {};\n\n",
        json_string_literal(HARN_AGENT_EVENT_METHOD)
    ));
    out.push_str("/// JSON-RPC method for Harn's provider catalog extension.\n");
    out.push_str(&format!(
        "pub const HARN_PROVIDER_CATALOG_METHOD: &str = {};\n\n",
        json_string_literal(HARN_PROVIDER_CATALOG_METHOD)
    ));

    out.push_str(&rust_const_group(
        "ACP_AGENT_METHOD",
        "ACP_AGENT_METHODS",
        "Stable host-facing ACP agent methods (matches the TypeScript/Swift/Python/Go bindings).",
        ACP_AGENT_METHODS,
    ));
    out.push_str(&rust_const_group(
        "ACP_DISPATCHED_METHOD",
        "ACP_DISPATCHED_METHODS",
        "Every JSON-RPC method the ACP adapter actually dispatches, including the \
         workspace-management, workflow-control, and HITL methods the stable \
         bindings do not yet expose as typed enums. Reconciled against the \
         `match` arms in `harn-serve`'s ACP adapter.",
        ACP_DISPATCHED_METHODS,
    ));
    out.push_str(&rust_const_group(
        "ACP_CLIENT_METHOD",
        "ACP_CLIENT_METHODS",
        "ACP client methods the agent calls back into the host for.",
        ACP_CLIENT_METHODS,
    ));
    out.push_str(&rust_const_group(
        "ACP_AGENT_NOTIFICATION",
        "ACP_AGENT_NOTIFICATIONS",
        "ACP notifications the agent emits to the host.",
        ACP_AGENT_NOTIFICATIONS,
    ));

    let session_updates = all_acp_session_updates();
    out.push_str(&rust_const_group_owned(
        "ACP_SESSION_UPDATE",
        "ACP_SESSION_UPDATES",
        "Every `session/update` discriminator Harn emits (canonical ACP variants \
         plus Harn extensions), the union the Swift binding exposes as \
         `acpSessionUpdateExtensions` merged with the base ACP set.",
        &session_updates,
    ));
    out.push_str(&rust_const_group(
        "HARN_ACP_SESSION_UPDATE_EXTENSION",
        "HARN_ACP_SESSION_UPDATE_EXTENSIONS",
        "Harn-specific `session/update` discriminators beyond the canonical ACP \
         set (the values Swift publishes as `acpSessionUpdateExtensions`).",
        HARN_SESSION_UPDATE_EXTENSIONS,
    ));
    out.push_str(&rust_const_group(
        "HARN_AGENT_EVENT_KIND",
        "HARN_AGENT_EVENT_KINDS",
        "Pipeline-loop milestone kinds emitted via `_harn/agentEvent`.",
        HARN_AGENT_EVENT_KINDS,
    ));
    out.push_str(&rust_const_group(
        "HARN_CONTENT_EXTENSION_FIELD",
        "HARN_CONTENT_EXTENSION_FIELDS",
        "`_meta.harn` content-block extension keys (`visible_text` / \
         `visible_delta`) Harn attaches to streamed content.",
        HARN_CONTENT_EXTENSION_FIELDS,
    ));
    out.push_str(&rust_const_group(
        "HARN_TOOL_LIFECYCLE_EXTENSION_FIELD",
        "HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS",
        "`_meta.harn` tool-lifecycle extension keys on tool_call / \
         tool_call_update notifications.",
        HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS,
    ));

    out
}

/// Render a `pub const` per wire value plus a published slice of all of them.
pub(super) fn rust_const_group(
    const_prefix: &str,
    slice_name: &str,
    doc: &str,
    values: &[&str],
) -> String {
    rust_const_group_owned(const_prefix, slice_name, doc, &strs_to_strings(values))
}

pub(super) fn rust_const_group_owned(
    const_prefix: &str,
    slice_name: &str,
    doc: &str,
    values: &[String],
) -> String {
    let mut out = String::new();
    for value in values {
        out.push_str(&format!(
            "pub const {}: &str = {};\n",
            rust_const_name(const_prefix, value),
            json_string_literal(value)
        ));
    }
    out.push('\n');
    out.push_str(&rust_doc_comment(doc));
    out.push_str(&format!("pub const {slice_name}: &[&str] = &[\n"));
    for value in values {
        out.push_str("    ");
        out.push_str(&json_string_literal(value));
        out.push_str(",\n");
    }
    out.push_str("];\n\n");
    out
}

/// Build a valid SCREAMING_SNAKE_CASE Rust const identifier from a wire value.
///
/// Wire values carry `/`, `.`, `-`, and `_` separators (e.g. `session/prompt`,
/// `harn.hitl.respond`, `tool_call_update`); all collapse to `_`, runs are
/// merged, and a leading digit is prefixed with `_`.
pub(super) fn rust_const_name(prefix: &str, value: &str) -> String {
    let mut suffix = String::with_capacity(value.len());
    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() {
            suffix.extend(ch.to_uppercase());
        } else {
            suffix.push('_');
        }
    }
    let suffix = collapse_repeated_underscores(suffix.trim_matches('_'));
    let name = if suffix.is_empty() {
        prefix.to_string()
    } else {
        format!("{prefix}_{suffix}")
    };
    if name.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
        format!("_{name}")
    } else {
        name
    }
}

pub(super) fn rust_doc_comment(doc: &str) -> String {
    let mut out = String::new();
    // Collapse the whitespace that Rust string continuations introduce so the
    // emitted doc comment is a single tidy line.
    let normalized = doc.split_whitespace().collect::<Vec<_>>().join(" ");
    out.push_str("/// ");
    out.push_str(&normalized);
    out.push('\n');
    out
}