include!(concat!(env!("OUT_DIR"), "/help_generated.rs"));
pub struct HelpNode {
pub path: &'static str,
pub title: &'static str,
pub excerpt: &'static str,
pub last_updated: &'static str,
pub content: &'static str,
}
pub fn top_level_sections() -> Vec<(String, String, String)> {
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
for node in HELP.iter() {
let top = node.path.split('/').next().unwrap_or("").trim();
if top.is_empty() || !seen.insert(top.to_string()) {
continue;
}
let title = HELP
.iter()
.find(|n| n.path == format!("{}/index", top) || n.path == top)
.map(|n| n.title.to_string())
.unwrap_or_else(|| top.to_string());
let content = get_help(top).unwrap_or_else(|_| format!("# {title}\n"));
out.push((top.to_string(), title, content));
}
out
}
fn official_node_definitions_for_help() -> Vec<crate::pipeline::NodeDefinition> {
let mut defs = crate::pipeline::nodes::builtin_node_definitions();
defs.extend(
crate::platform::services::node_registry::NodeRegistryService::embedded_official_definitions(),
);
defs.sort_by(|a, b| a.kind.cmp(&b.kind));
defs
}
fn official_nodes_markdown_reference_for_help() -> String {
let mut s = String::from(
"## Node kinds (live — native + embedded official composites)\n\n\
This block matches the pipeline editor / project node API for platform-bundled nodes: titles, descriptions, pins, DSL flags, and input/output schemas.\n\n\
- **Full catalog:** `help_nodes` with no `kind` (same as this section).\n\
- **One kind:** `help_nodes` with `kind=\"n.script\"` (or `script`, `trigger.webhook`, composite kinds, etc.).\n\n\
---\n\n",
);
for def in official_node_definitions_for_help() {
s.push_str(&crate::pipeline::nodes::format_node_definition_markdown(
&def,
));
s.push_str("---\n\n");
}
s
}
pub fn get_help(path: &str) -> Result<String, String> {
let path = path.trim_matches('/').trim();
if path.is_empty() {
return Ok(help_root_index());
}
if path == "pipeline/nodes" {
return Ok(official_nodes_markdown_reference_for_help());
}
if let Some(kind) = path.strip_prefix("pipeline/nodes/") {
return match official_node_definitions_for_help()
.into_iter()
.find(|def| crate::pipeline::nodes::kind_query_matches_def(def, kind))
{
Some(def) => Ok(crate::pipeline::nodes::format_node_definition_markdown(
&def,
)),
None => Err(format!(
"Node '{}' not found. Call help(\"pipeline/nodes\") for the full catalog.",
kind
)),
};
}
let node = HELP
.iter()
.find(|n| n.path == path)
.or_else(|| HELP.iter().find(|n| n.path == &format!("{}/index", path)));
if let Some(node) = node {
let mut out = node.content.to_string();
let children = children_of(path);
if !children.is_empty() {
out.push_str("\n\n---\n\n## Sub-topics\n\n");
out.push_str(&format_children_index(path, &children));
}
return Ok(out);
}
if path == "pipeline/examples" {
let examples: Vec<&HelpNode> = HELP
.iter()
.filter(|n| n.path.starts_with("pipeline/examples/"))
.collect();
if !examples.is_empty() {
let mut out = "# Pipeline Examples\n\nCall `help(\"pipeline/examples/{slug}\")` to load a full recipe.\n\n".to_string();
out.push_str("| Example | Description |\n|---------|-------------|\n");
for ex in examples {
let slug = ex.path.trim_start_matches("pipeline/examples/");
out.push_str(&format!(
"| `help(\"pipeline/examples/{}\")` | {} |\n",
slug, ex.title
));
}
return Ok(out);
}
}
let closest = closest_paths(path);
let suggestions = if closest.is_empty() {
"Call help() with no argument for the full index.".to_string()
} else {
format!(
"Did you mean one of:\n{}",
closest
.iter()
.map(|p| format!(" help(\"{}\")", p))
.collect::<Vec<_>>()
.join("\n")
)
};
Err(format!("Help topic '{}' not found. {}", path, suggestions))
}
pub fn get_help_content(path: &str) -> Option<String> {
get_help(path).ok()
}
pub fn help_root_index() -> String {
let mut out = String::from(
"# Zebflow Help Index\n\n\
Call `help(topic)` for documentation.\n\n\
| Topic | Title | Last updated |\n\
|-------|-------|--------------|\n",
);
let mut top_levels: Vec<&str> = Vec::new();
let mut seen_tops = std::collections::HashSet::new();
for node in HELP.iter() {
let top = node.path.splitn(2, '/').next().unwrap_or("");
if !top.is_empty() && seen_tops.insert(top) {
top_levels.push(top);
}
}
for top in top_levels {
if let Some(idx) = HELP
.iter()
.find(|n| n.path == &format!("{}/index", top) || n.path == top)
{
let display = if idx.path.ends_with("/index") {
top
} else {
idx.path
};
out.push_str(&format!(
"| **`help(\"{}\")`** | **{}** | {} |\n",
display, idx.title, idx.last_updated
));
}
let prefix = format!("{}/", top);
let mut children: Vec<&HelpNode> = HELP
.iter()
.filter(|n| {
n.path.starts_with(&prefix)
&& !n.path.ends_with("/index")
&& !n.path[prefix.len()..].contains('/')
})
.collect();
children.sort_by_key(|n| n.path);
for child in children {
out.push_str(&format!(
"| `help(\"{}\")` | {} | {} |\n",
child.path, child.title, child.last_updated
));
}
if top == "pipeline" {
out.push_str(
"| `help(\"pipeline/nodes\")` | **Node Catalog** (live from Rust) | live |\n",
);
out.push_str("| `help(\"pipeline/examples\")` | Pipeline Examples Index | — |\n");
}
}
out.push_str(
"\n`help_search(query)` — search across all help docs + node catalog (partial, scored)\n\n\
Tip: `help(\"pipeline/nodes\")` → full node catalog with all DSL flags and schemas.",
);
out
}
pub fn all_searchable_content() -> Vec<(String, String, String)> {
let mut all: Vec<(String, String, String)> = HELP
.iter()
.map(|n| {
(
n.path.to_string(),
n.title.to_string(),
n.content.to_string(),
)
})
.collect();
for def in official_node_definitions_for_help() {
let path = format!("pipeline/nodes/{}", def.kind);
let title = format!("{} — {}", def.kind, def.title);
let mut content = format!(
"kind: {}\ntitle: {}\n{}\n",
def.kind, def.title, def.description
);
for flag in &def.dsl_flags {
content.push_str(&format!(
"flag: {} key: {} required: {} {}\n",
flag.flag, flag.config_key, flag.required, flag.description
));
}
all.push((path, title, content));
}
all
}
pub fn format_for_system_prompt() -> String {
HELP.iter()
.map(|n| format!("### {}\n{}\n", n.title, n.content))
.collect::<Vec<_>>()
.join("\n---\n\n")
}
fn children_of(parent: &str) -> Vec<&'static HelpNode> {
let prefix = format!("{}/", parent);
HELP.iter()
.filter(|n| {
n.path.starts_with(&prefix) && {
let rest = &n.path[prefix.len()..];
!rest.contains('/') && rest != "index"
}
})
.collect()
}
fn format_children_index(parent: &str, children: &[&HelpNode]) -> String {
let mut out = String::new();
for child in children {
let slug = &child.path[parent.len() + 1..];
out.push_str(&format!(
"- `help(\"{}\")` — **{}** — {}\n",
child.path, child.title, child.excerpt
));
let _ = slug; }
if parent == "pipeline" {
out.push_str("- `help(\"pipeline/nodes\")` — **Node Catalog** — All built-in nodes (live from Rust)\n");
out.push_str("- `help(\"pipeline/examples\")` — **Examples Index** — Full DSL recipes\n");
}
out
}
fn closest_paths(query: &str) -> Vec<&'static str> {
let q = query.to_lowercase();
let mut hits: Vec<&'static str> = HELP
.iter()
.filter(|n| n.path.contains(&*q) || n.title.to_lowercase().contains(&*q))
.map(|n| n.path)
.take(5)
.collect();
if hits.is_empty() {
hits = HELP
.iter()
.filter(|n| !n.path.contains('/'))
.map(|n| n.path)
.take(5)
.collect();
}
hits
}