harn-cli 0.10.132

CLI for the Harn programming language — run, test, REPL, format, and lint
//! Recap write-schema fields projected into the shared host record model.

use super::records::{FieldKind, Integer, Record, Target};
use serde_json::Value;

const RECORDS: &[(&str, &str)] = &[
    ("query", "Query"),
    ("cursor", "Cursor"),
    ("coverage", "Coverage"),
    ("sourceEvent", "SourceEvent"),
    ("source", "Source"),
    ("textFact", "TextFact"),
    ("verification", "VerificationFact"),
    ("toolExchange", "ToolExchange"),
    ("planStep", "PlanStep"),
    ("planEvent", "PlanEventFact"),
    ("planFact", "PlanFact"),
    ("progressEntry", "ProgressEntry"),
    ("progressFact", "ProgressFact"),
    ("terminalFact", "TerminalFact"),
    ("iteration", "Iteration"),
    ("turn", "PromptTurnRecap"),
    ("snapshot", "Snapshot"),
];

const ENUMS: &[(&str, &str)] = &[
    ("CompletionState", "/$defs/completionState/enum"),
    ("ToolState", "/$defs/toolExchange/properties/state/enum"),
    ("PlanStepStatus", "/$defs/planStep/properties/status/enum"),
    ("PlanEventKind", "/$defs/planEvent/properties/kind/enum"),
    (
        "ProgressStatus",
        "/$defs/progressEntry/properties/status/enum",
    ),
    (
        "ProgressPriority",
        "/$defs/progressEntry/properties/priority/oneOf/0/enum",
    ),
    (
        "VerificationStatus",
        "/$defs/verification/properties/status/const",
    ),
    ("UnavailableReason", "/oneOf/1/properties/reason/enum"),
];

pub(super) fn append_enums(out: &mut String, target: Target) {
    let schema = harn_vm::session_recap::session_recap_json_schema();
    for (suffix, pointer) in ENUMS {
        let value = schema
            .pointer(pointer)
            .expect("registered recap vocabulary exists");
        let values = if let Some(values) = value.as_array() {
            values.clone()
        } else {
            vec![value.clone()]
        };
        let values = values
            .iter()
            .map(|value| {
                value
                    .as_str()
                    .expect("recap vocabulary is strings")
                    .to_owned()
            })
            .collect::<Vec<_>>();
        append_enum(out, target, suffix, &values);
    }
    if matches!(target, Target::Swift | Target::Python) {
        let values = schema["oneOf"]
            .as_array()
            .expect("recap availability variants")
            .iter()
            .map(|variant| {
                variant["properties"]["state"]["const"]
                    .as_str()
                    .expect("recap availability discriminator")
                    .to_owned()
            })
            .collect::<Vec<_>>();
        append_enum(out, target, "AvailabilityState", &values);
    }
}

fn append_enum(out: &mut String, target: Target, suffix: &str, values: &[String]) {
    let name = format!("HarnSessionRecap{suffix}");
    match target {
        Target::Typescript => out.push_str(&format!("export type {name} = {}\n", values.iter().map(|value| format!("{value:?}")).collect::<Vec<_>>().join(" | "))),
        Target::Rust => out.push_str(&format!("#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum {name} {{ {} }}\n", values.iter().map(|value| super::rust::rust_type_name(value)).collect::<Vec<_>>().join(", "))),
        Target::Swift => out.push_str(&format!("public enum {name}: String, Codable, Sendable, Equatable {{ case {} }}\n", values.iter().map(|value| {
            let case = super::swift::swift_case_name(value);
            if &case == value { case } else { format!("{case} = {value:?}") }
        }).collect::<Vec<_>>().join(", "))),
        Target::Python => out.push_str(&super::python::py_str_enum_owned(&name, values)),
        Target::Go => out.push_str(&format!("type {name} string\n")),
    }
}

fn record_name(key: &str) -> Option<String> {
    RECORDS
        .iter()
        .find(|(name, _)| *name == key)
        .map(|(_, suffix)| {
            if key == "turn" {
                "HarnSessionPromptTurnRecap".into()
            } else {
                format!("HarnSessionRecap{suffix}")
            }
        })
}

pub(super) fn load() -> Result<Vec<Record>, String> {
    from_schema(&harn_vm::session_recap::session_recap_json_schema())
}

fn from_schema(schema: &Value) -> Result<Vec<Record>, String> {
    let names = RECORDS
        .iter()
        .map(|(key, _)| (*key, record_name(key).expect("registered recap record")))
        .collect::<Vec<_>>();
    super::schema_records::SchemaRecords {
        schema,
        names: &names,
        label: "recap",
        require_all: true,
        metadata,
    }
    .load()
}

fn metadata(owner: &str, field: &str, schema: &Value) -> Result<Option<FieldKind>, String> {
    if schema.get("enum").is_some() || schema["const"].is_string() {
        let suffix = match (owner, field) {
            ("toolExchange", "state") => "ToolState",
            ("planStep", "status") => "PlanStepStatus",
            ("planEvent", "kind") => "PlanEventKind",
            ("progressEntry", "status") => "ProgressStatus",
            ("progressEntry", "priority") => "ProgressPriority",
            ("verification", "status") => "VerificationStatus",
            (_, "state") => "CompletionState",
            _ => return Err(format!("unmapped recap enum {owner}.{field}")),
        };
        return Ok(Some(FieldKind::Named(format!("HarnSessionRecap{suffix}"))));
    }
    if schema["type"] == "integer" || schema["const"].is_u64() {
        Ok(Some(FieldKind::Integer(match (owner, field) {
            ("snapshot", "schemaVersion") => Integer::U32,
            ("coverage", _) | ("query", "limit") => Integer::Usize,
            ("iteration", "iteration") => Integer::I64,
            _ => Integer::U64,
        })))
    } else {
        Ok(None)
    }
}

pub(super) fn append(out: &mut String, target: Target) {
    let records = load().expect("the owning recap schema projects to host records");
    for record in &records {
        record.append_closed(out, target);
        if matches!(target, Target::Python) {
            let conversions = record
                .fields
                .iter()
                .filter_map(|field| {
                    let value = format!("values[{:?}]", field.wire_name);
                    python_value(&field.kind, &value, &records)
                        .map(|conversion| format!("        {value} = {conversion}\n"))
                })
                .collect::<String>();
            if !conversions.is_empty() {
                out.push_str(&format!("    @classmethod\n    def from_wire(cls, data: Mapping[str, Any]) -> {:?}:\n        values = cls._strict_values(data)\n{conversions}        return cls(**values)\n\n", record.name));
            }
        }
    }
}

fn python_value(kind: &FieldKind, value: &str, records: &[Record]) -> Option<String> {
    match kind {
        FieldKind::Named(name) => Some(if records.iter().any(|record| &record.name == name) {
            format!("{name}.from_wire({value})")
        } else {
            format!("{name}({value})")
        }),
        FieldKind::List(inner) => python_value(inner, "item", records)
            .map(|expression| format!("[{expression} for item in {value}]")),
        FieldKind::Nullable(inner) => python_value(inner, value, records)
            .map(|expression| format!("None if {value} is None else {expression}")),
        _ => None,
    }
}

pub(super) fn append_validators(out: &mut String, target: Target) {
    let records = load().expect("the owning recap schema projects to host records");
    for record in &records {
        let name = &record.name;
        let keys = record
            .fields
            .iter()
            .map(|field| format!("{:?}", field.wire_name))
            .collect::<Vec<_>>()
            .join(", ");
        let mut body = String::new();
        for field in &record.fields {
            let value = format!("object[{:?}]", field.wire_name);
            body.push_str(&validation(&field.kind, &value, &records, target));
        }
        if name == "HarnSessionRecapVerificationFact" && matches!(target, Target::Typescript) {
            let schema = harn_vm::session_recap::session_recap_json_schema();
            let status = schema["$defs"]["verification"]["properties"]["status"]["const"]
                .as_str()
                .expect("verification status literal");
            body.push_str(&format!("  if (object.status !== {status:?}) throw new TypeError(\"Harn session recap verification status must be {status}\")\n"));
        }
        match target {
            Target::Typescript => {
                let binding = if body.is_empty() {
                    ""
                } else {
                    "const object = "
                };
                out.push_str(&format!("function validate{name}(value: unknown): void {{\n  {binding}harnSessionRecapObject(value, {name:?}, [{keys}])\n"));
            }
            Target::Swift => {
                let binding = if body.is_empty() { "_" } else { "let object" };
                out.push_str(&format!("private func validate{name}(_ value: HarnACPValue?) throws {{\n    {binding} = try harnSessionRecapObject(value, label: {name:?}, keys: [{keys}])\n"));
            }
            _ => unreachable!("only Swift and TypeScript need an explicit validation walk"),
        }
        out.push_str(&body);
        out.push_str("}\n\n");
    }
}

fn validation(kind: &FieldKind, value: &str, records: &[Record], target: Target) -> String {
    match kind {
        FieldKind::Named(name) if records.iter().any(|record| &record.name == name) => match target
        {
            Target::Typescript => format!("  validate{name}({value})\n"),
            Target::Swift => format!("    try validate{name}({value})\n"),
            _ => unreachable!(),
        },
        FieldKind::Nullable(inner) => {
            let body = validation(inner, value, records, target);
            if body.is_empty() {
                return body;
            }
            match target {
                Target::Typescript => format!("  if ({value} !== null) {{\n{body}  }}\n"),
                Target::Swift => format!("    if {value} != .null {{\n{body}    }}\n"),
                _ => unreachable!(),
            }
        }
        FieldKind::List(inner) => {
            let body = validation(inner, "item", records, target);
            if body.is_empty() {
                return body;
            }
            match target {
                Target::Typescript => format!("  for (const item of harnSessionRecapArray({value}, {value:?})) {{\n{body}  }}\n"),
                Target::Swift => format!("    for item in try harnSessionRecapArray({value}, label: {value:?}) {{\n{body}    }}\n"),
                _ => unreachable!(),
            }
        }
        _ => String::new(),
    }
}

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

    #[test]
    fn rejects_duplicate_inventory_and_recursive_aliases() {
        let mut schema = harn_vm::session_recap::session_recap_json_schema();
        schema["$defs"]["query"]["required"][1] = Value::String("sessionId".into());
        assert_eq!(
            from_schema(&schema).unwrap_err(),
            "recap query repeats a required field"
        );
        schema = harn_vm::session_recap::session_recap_json_schema();
        schema["$defs"]["nullableString"] = serde_json::json!({"$ref": "#/$defs/nullableString"});
        assert_eq!(
            from_schema(&schema).unwrap_err(),
            "cyclic recap reference #/$defs/nullableString"
        );
    }
}