use serde_json::{Value, json};
pub(crate) fn classify_error_code(message: &str) -> &'static str {
let lower = message.to_ascii_lowercase();
if lower.contains("invalid value")
|| lower.contains("unexpected argument")
|| lower.contains("required arguments")
|| lower.contains("usage:")
{
return "QF_INVALID_ARGUMENT";
}
if lower.contains("failed to read") || lower.contains("no such file") || lower.contains("not found") {
return "QF_INPUT_NOT_FOUND";
}
if lower.contains("clipboard") {
return "QF_CLIPBOARD_ERROR";
}
if lower.contains("pivot query syntax error") || lower.contains("sql") {
return "QF_SQL_ERROR";
}
if lower.contains("schema") && lower.contains("diff") {
return "QF_SCHEMA_MISMATCH";
}
if lower.contains("format") {
return "QF_UNSUPPORTED_FORMAT";
}
"QF_RUNTIME_ERROR"
}
pub(crate) fn query_value_to_json(value: &query_forge::QueryValue) -> Value {
match value {
query_forge::QueryValue::Null => Value::Null,
query_forge::QueryValue::Integer(v) => json!(v),
query_forge::QueryValue::Real(v) => json!(v),
query_forge::QueryValue::Text(v) => json!(v),
}
}
pub(crate) fn query_result_to_json(result: &query_forge::QueryResult) -> Value {
let rows = result
.rows
.iter()
.map(|row| {
let mut obj = serde_json::Map::new();
for (col, val) in result.columns.iter().zip(row.iter()) {
obj.insert(col.clone(), query_value_to_json(val));
}
Value::Object(obj)
})
.collect::<Vec<_>>();
json!({
"columns": result.columns,
"rows": rows,
"row_count": result.rows.len(),
})
}
pub(crate) fn table_summaries_to_json(
summaries: &[query_forge::TableSummary],
include_samples: bool,
include_stats: bool,
) -> Value {
let tables = summaries
.iter()
.map(|summary| {
let sample_rows = if include_samples {
summary
.sample_rows
.iter()
.map(|row| {
row.iter()
.map(query_value_to_json)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let columns = summary
.columns
.iter()
.enumerate()
.map(|(idx, col)| {
let mut value = json!({
"name": col.column_name,
"type": col.inferred_type.as_str(),
"nullable": col.nullable,
});
if include_stats {
if let Some(stats) = summary.column_stats.as_ref().and_then(|all| all.get(idx)) {
value["stats"] = json!({
"distinct_count": stats.distinct_count,
"min": stats.min_value,
"max": stats.max_value,
});
}
}
value
})
.collect::<Vec<_>>();
json!({
"table": summary.table_name,
"source": summary.source_path,
"selector": summary.source_selector,
"row_count": summary.row_count,
"columns": columns,
"warnings": summary.warnings,
"sample_rows": sample_rows,
})
})
.collect::<Vec<_>>();
json!(tables)
}
pub(crate) fn ai_success_envelope(
command: &str,
data: Value,
metadata: Value,
warnings: Vec<String>,
) -> Value {
json!({
"ok": true,
"schema_version": "1.0",
"command": command,
"data": data,
"metadata": metadata,
"warnings": warnings,
"error": Value::Null,
})
}
pub(crate) fn ai_error_envelope(code: &str, message: &str) -> Value {
json!({
"ok": false,
"schema_version": "1.0",
"command": Value::Null,
"data": Value::Null,
"metadata": Value::Null,
"warnings": [],
"error": {
"code": code,
"message": message,
},
})
}
pub(crate) fn capabilities_json() -> Value {
json!({
"tool": "query-forge",
"schema_version": "1.0",
"ai": {
"ai_mode": {
"flag": "--ai-mode",
"description": "Wraps command results in a structured JSON envelope for AI agents.",
},
"error_codes": [
"QF_INVALID_ARGUMENT",
"QF_INPUT_NOT_FOUND",
"QF_CLIPBOARD_ERROR",
"QF_SQL_ERROR",
"QF_SCHEMA_MISMATCH",
"QF_UNSUPPORTED_FORMAT",
"QF_RUNTIME_ERROR"
]
},
"commands": {
"query": {
"supports_ai_mode": true,
"supports_meta": true,
"meta_formats": ["text", "json"],
"output_formats": ["table", "csv", "json", "jsonl", "markdown", "html", "xml", "xlsx", "feather", "parquet"]
},
"tables": {
"supports_ai_mode": true,
"output_formats": ["text", "json", "markdown"]
},
"schema": {
"supports_ai_mode": true,
"output_formats": ["text", "json", "markdown"]
},
"inspect": {
"supports_ai_mode": true,
"output_formats": ["text", "json", "markdown"]
},
"diff": {
"supports_ai_mode": true,
"output_formats": ["table", "csv", "json", "jsonl", "markdown", "html"]
},
"pivot": {
"supports_ai_mode": true,
"output_formats": ["table", "csv", "json", "jsonl", "markdown", "html", "xml", "xlsx", "feather", "parquet"]
},
"conv": {
"supports_ai_mode": true,
"output_formats": ["table", "csv", "json", "jsonl", "markdown", "html", "xml", "xlsx", "feather", "parquet"]
},
"completions": {
"supports_ai_mode": false
},
"man-page": {
"supports_ai_mode": false
},
"capabilities": {
"supports_ai_mode": false,
"output": "json"
}
}
})
}