tandem-plan-compiler 0.6.3

Mission and plan compiler boundary for Tandem
fn value_string_array(value: &Value) -> Vec<String> {
    match value {
        Value::String(text) => text
            .split(',')
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        Value::Array(items) => items
            .iter()
            .filter_map(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        _ => Vec::new(),
    }
}

pub fn normalize_connector_writer_metadata_value(metadata: &mut Value) -> bool {
    let Some(root) = metadata.as_object_mut() else {
        return false;
    };
    let has_notion_target = root.contains_key("notion_data_source_id")
        || root.contains_key("notion_data_source_url")
        || root.contains_key("notion_property_mappings")
        || root.contains_key("notion_default_properties");
    let explicit_writer = root
        .get("connector_writer")
        .and_then(Value::as_bool)
        .unwrap_or(false);
    let connector = root
        .get("connector")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_ascii_lowercase);
    let connector_is_notion = has_notion_target || connector.as_deref() == Some("notion");
    if !explicit_writer && !has_notion_target {
        return false;
    }

    let mut changed = false;
    if !explicit_writer {
        root.insert("connector_writer".to_string(), Value::Bool(true));
        changed = true;
    }
    if connector.is_none() && has_notion_target {
        root.insert("connector".to_string(), Value::String("notion".to_string()));
        changed = true;
    }
    if !root.contains_key("connector_writer_contract_version") {
        root.insert(
            "connector_writer_contract_version".to_string(),
            Value::Number(1u64.into()),
        );
        changed = true;
    }
    if !root.contains_key("writer_kind") {
        root.insert(
            "writer_kind".to_string(),
            Value::String("database_rows".to_string()),
        );
        changed = true;
    }
    if connector_is_notion && !root.contains_key("update_after_create") {
        root.insert("update_after_create".to_string(), Value::Bool(true));
        changed = true;
    }
    if !root.contains_key("property_mappings") {
        if let Some(value) = root.get("notion_property_mappings").cloned() {
            root.insert("property_mappings".to_string(), value);
            changed = true;
        }
    }
    if !root.contains_key("default_properties") {
        if let Some(value) = root.get("notion_default_properties").cloned() {
            root.insert("default_properties".to_string(), value);
            changed = true;
        }
    }
    for key in ["duplicate_keys", "source_array_key", "source_array_path"] {
        if let Some(value) = root.get(key).cloned() {
            let values = value_string_array(&value);
            if !values.is_empty() && !matches!(value, Value::Array(_)) {
                if key == "source_array_key" || key == "source_array_path" {
                    if values.len() == 1 {
                        root.insert(key.to_string(), Value::String(values[0].clone()));
                    } else {
                        root.insert(
                            format!("{key}s"),
                            Value::Array(values.into_iter().map(Value::String).collect()),
                        );
                    }
                } else {
                    root.insert(
                        key.to_string(),
                        Value::Array(values.into_iter().map(Value::String).collect()),
                    );
                }
                changed = true;
            }
        }
    }
    changed
}

fn planner_mcp_server_segment(name: &str) -> String {
    name.trim()
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_lowercase()
            } else {
                '_'
            }
        })
        .collect::<String>()
        .trim_matches('_')
        .to_string()
}

pub fn planner_connector_writer_contracts(server_tools: &[PlannerMcpServerToolSet]) -> Vec<Value> {
    let mut contracts = Vec::new();
    for server in server_tools {
        let segment = planner_mcp_server_segment(&server.server);
        let tools = server
            .tool_names
            .iter()
            .map(|tool| tool.trim().to_ascii_lowercase())
            .collect::<Vec<_>>();
        let has_notion_fetch = tools
            .iter()
            .any(|tool| tool == "mcp.notion.notion_fetch" || tool.ends_with(".notion_fetch"));
        let has_notion_search = tools
            .iter()
            .any(|tool| tool == "mcp.notion.notion_search" || tool.ends_with(".notion_search"));
        let has_notion_create = tools.iter().any(|tool| {
            tool == "mcp.notion.notion_create_pages" || tool.ends_with(".notion_create_pages")
        });
        let has_notion_update = tools.iter().any(|tool| {
            tool == "mcp.notion.notion_update_page" || tool.ends_with(".notion_update_page")
        });
        if segment == "notion"
            && has_notion_fetch
            && has_notion_search
            && has_notion_create
            && has_notion_update
        {
            contracts.push(json!({
                "connector": "notion",
                "writer_kind": "database_rows",
                "required_tools": [
                    "mcp.notion.notion_fetch",
                    "mcp.notion.notion_search",
                    "mcp.notion.notion_create_pages",
                    "mcp.notion.notion_update_page"
                ],
                "required_metadata": [
                    "connector",
                    "connector_writer",
                    "writer_kind",
                    "input_alias",
                    "source_array_key or source_array_path",
                    "notion_data_source_url or notion_data_source_id",
                    "title_property",
                    "property_mappings",
                    "duplicate_keys"
                ],
                "metadata_example": {
                    "connector": "notion",
                    "connector_writer": true,
                    "writer_kind": "database_rows",
                    "notion_data_source_url": "collection://...",
                    "input_alias": "filtered_rows",
                    "source_array_key": "rows",
                    "title_property": "Name",
                    "property_mappings": {"Name": "title", "URL": "url"},
                    "duplicate_keys": ["URL"]
                }
            }));
        }
    }
    contracts
}