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
}
pub fn official_nodes_index_markdown() -> String {
let defs = official_node_definitions_for_help();
let mut families: std::collections::BTreeMap<String, Vec<&crate::pipeline::NodeDefinition>> = std::collections::BTreeMap::new();
for d in &defs {
let short = crate::platform::shell::parser::short_kind(&d.kind);
let family = short.split('.').next().unwrap_or(&short).to_string();
families.entry(family).or_default().push(d);
}
let mut s = format!(
"# Node index — {} kinds\n\n\
One line per node: `short name` — title — what it does. One node in full (flags, pins, schemas, examples): \
`help topic=\"pipeline/nodes/<short name>\"`. The whole catalogue at once: `help topic=\"pipeline/nodes/all\"` (large). \
In the DSL a node is written by its short name: `| web.response --template pages/x.tsx`.\n",
defs.len()
);
for (family, kinds) in families {
s.push_str(&format!("\n## {family}\n"));
let mut kinds = kinds;
kinds.sort_by(|a, b| a.kind.cmp(&b.kind));
for d in kinds {
let short = crate::platform::shell::parser::short_kind(&d.kind);
let first = d.description.split(". ").next().unwrap_or(&d.description).trim().trim_end_matches('.');
s.push_str(&format!("- `{short}` — {} — {}\n", d.title.trim(), clip_words(first, 120)));
}
}
s
}
fn clip_words(text: &str, max: usize) -> String {
if text.chars().count() <= max {
return text.to_string();
}
let head: String = text.chars().take(max).collect();
let cut = head.rfind(' ').unwrap_or(head.len());
format!("{}…", head[..cut].trim_end_matches([',', ';', ':', '(', '-']))
}
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_index_markdown());
}
if path == "pipeline/nodes/all" {
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 = expand_definition_markers(node.content);
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 expand_sekejap_markers(content: &str) -> String {
let version = crate::version::SEKEJAP_ENGINE_VERSION;
let repo = "https://github.com/sekejapdb/sekejap";
let links = format!(
"- This engine (sekejap {version}): [QL_CONTRACT.md]({repo}/blob/v{version}/docs/lang/QL_CONTRACT.md) · \
[INDEX_CONTRACT.md]({repo}/blob/v{version}/docs/lang/INDEX_CONTRACT.md) · \
[CHANGELOG]({repo}/blob/v{version}/CHANGELOG.md)\n\
- Latest (main, may be ahead of this engine — do not use a feature it lists unless the pinned grammar has it): \
[QL_CONTRACT.md]({repo}/blob/main/docs/lang/QL_CONTRACT.md)"
);
content
.replace("<!-- sekejap-version -->", version)
.replace("<!-- sekejap-grammar -->", &links)
}
pub fn expand_definition_markers(content: &str) -> String {
let mut out = expand_sekejap_markers(content);
let defs = official_node_definitions_for_help();
let marker = regex::Regex::new(r"<!-- node-flags:([a-z0-9_.]+) -->").unwrap();
let rendered = marker
.replace_all(&out, |caps: ®ex::Captures| {
let kind = &caps[1];
match defs.iter().find(|d| crate::pipeline::nodes::kind_query_matches_def(d, kind)) {
Some(def) => format_node_flags_markdown(def),
None => format!("<!-- node-flags:{kind} — unknown node -->"),
}
})
.to_string();
out = rendered;
if out.contains("<!-- node-families -->") {
out = out.replace("<!-- node-families -->", &format_node_families_markdown(&defs));
}
out
}
pub fn format_node_flags_markdown(def: &crate::pipeline::NodeDefinition) -> String {
use crate::pipeline::model::DslFlagKind;
let short = crate::platform::shell::parser::short_kind(&def.kind);
let mut s = format!("| `{short}` flag | Meaning |
|---|---|
");
if def.dsl_flags.is_empty() {
s.push_str("| *(none)* | this node takes no flags |
");
}
for f in &def.dsl_flags {
let kind = match f.kind {
DslFlagKind::Scalar => "",
DslFlagKind::CommaSeparatedList => " · comma list",
DslFlagKind::RepeatedList => " · repeatable",
DslFlagKind::Bool => " · switch",
DslFlagKind::KeyValuePairs => " · `K=V`, repeatable",
DslFlagKind::SchemaField => " · schema field",
};
let req = if f.required { " **required**" } else { "" };
s.push_str(&format!("| `{}`{}{} | {} |
", f.flag, req, kind, f.description.replace('\n', " ").replace('|', "\\|")));
}
s
}
pub fn format_node_families_markdown(defs: &[crate::pipeline::NodeDefinition]) -> String {
let mut families: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
for d in defs {
let short = crate::platform::shell::parser::short_kind(&d.kind);
let family = short.split('.').next().unwrap_or(&short).to_string();
families.entry(family).or_default().push(short);
}
let mut s = String::new();
for (family, mut kinds) in families {
kinds.sort();
kinds.dedup();
s.push_str(&format!("- **{family}** `{}`\n", kinds.join(" · ")));
}
s
}
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
}
#[cfg(test)]
mod help_lint {
use super::*;
use crate::platform::shell::parser::{build_pipeline_graph_with_definitions, expand_kind};
fn code_blocks(content: &str) -> Vec<(usize, String)> {
let mut blocks = Vec::new();
let mut open: Option<(usize, Vec<&str>)> = None;
for (i, line) in content.lines().enumerate() {
if line.trim_start().starts_with("```") {
match open.take() {
Some((start, lines)) => blocks.push((start + 1, lines.join("\n"))),
None => open = Some((i, Vec::new())),
}
} else if let Some((_, lines)) = open.as_mut() {
lines.push(line);
}
}
blocks
}
fn pipeline_bodies(block: &str) -> Vec<String> {
let mut bodies: Vec<String> = Vec::new();
let mut current: Option<String> = None;
for raw in block.lines() {
let line = raw.trim();
let sketch = line.contains('…') || line.contains("...") || line.contains("<") && line.contains(">");
if let Some(rest) = line.strip_prefix("register ") {
if let Some(cur) = current.take() { bodies.push(cur); }
if sketch { continue; }
if let Some((_, body)) = rest.split_once(" -- ") { current = Some(body.trim().to_string()); }
} else if line.starts_with('|') {
if sketch { if let Some(cur) = current.take() { bodies.push(cur); } continue; }
match current.as_mut() {
Some(cur) => { cur.push(' '); cur.push_str(line); }
None => current = Some(line.to_string()),
}
} else if let Some(cur) = current.take() {
bodies.push(cur);
}
}
if let Some(cur) = current.take() { bodies.push(cur); }
bodies.into_iter().filter(|b| b.starts_with('|')).collect()
}
#[test]
fn every_dsl_example_in_the_help_registers_against_the_live_catalogue() {
let defs = official_node_definitions_for_help();
let mut failures = Vec::new();
let mut checked = 0usize;
for page in HELP.iter() {
for (line, block) in code_blocks(page.content) {
for body in pipeline_bodies(&block) {
checked += 1;
if let Err(e) = build_pipeline_graph_with_definitions("help-lint", &body, &defs) {
failures.push(format!("{}:{line}: {e}\n {body}", page.path));
}
}
}
}
assert!(failures.is_empty(), "help examples that no longer register:\n{}", failures.join("\n"));
assert!(checked >= 40, "the lint found only {checked} pipeline bodies in the help — the extractor is broken, not the help");
}
#[test]
fn every_node_the_help_names_exists() {
let defs = official_node_definitions_for_help();
let families: std::collections::BTreeSet<String> = defs
.iter()
.filter_map(|d| d.kind.strip_prefix("n.").and_then(|k| k.split('.').next()).map(str::to_string))
.collect();
let exists = |name: &str| {
expand_kind(name).is_some_and(|k| defs.iter().any(|d| d.kind == k))
|| defs.iter().any(|d| crate::pipeline::nodes::kind_query_matches_def(d, name))
};
let token = regex::Regex::new(r"`(n\.)?([a-z][a-z0-9_]*)((?:\.[a-z][a-z0-9_]*)+)`").unwrap();
let mut failures = Vec::new();
let mut checked = 0usize;
for page in HELP.iter() {
for (i, line) in page.content.lines().enumerate() {
for caps in token.captures_iter(line) {
let family = &caps[2];
let name = format!("{}{}{}", caps.get(1).map(|m| m.as_str()).unwrap_or(""), family, &caps[3]);
if name.ends_with(".*") { continue; }
if !families.contains(family) && !name.starts_with("n.") { continue; }
if family == "input" && !name.starts_with("n.") && !exists(&name) { continue; }
checked += 1;
if !exists(&name) {
failures.push(format!("{}:{}: `{name}`", page.path, i + 1));
}
}
}
}
assert!(failures.is_empty(), "help names nodes the catalogue does not have:\n{}", failures.join("\n"));
assert!(checked >= 100, "the lint checked only {checked} node names — the extractor is broken, not the help");
}
#[test]
fn markers_render_the_definition_not_a_copy() {
let web = get_help("pipeline/web").expect("pipeline/web");
assert!(!web.contains("<!-- node-flags:"), "the marker must be replaced");
assert!(web.contains("| `--file`"), "the flag table comes from the definition:\n{web}");
assert!(web.contains("| `--template`"));
let dsl = get_help("pipeline/dsl").expect("pipeline/dsl");
assert!(!dsl.contains("<!-- node-families -->"));
assert!(dsl.contains("- **web** `web.docs.generate · web.response · web.static.generate`"), "{dsl}");
assert!(dsl.contains("- **trigger** `"));
}
#[test]
fn the_node_index_lists_every_kind_once_and_stays_small() {
let defs = official_node_definitions_for_help();
let index = get_help("pipeline/nodes").expect("index");
for d in &defs {
let short = crate::platform::shell::parser::short_kind(&d.kind);
let needle = format!("- `{short}` — ");
assert_eq!(index.matches(&needle).count(), 1, "{short} must appear exactly once in the index");
}
assert!(index.len() < 12_000, "the index is {} bytes; it must stay a cheap read", index.len());
let all = get_help("pipeline/nodes/all").expect("catalogue");
for d in &defs {
assert!(all.contains(&format!("### `{}`", d.kind)), "{} missing from the full catalogue", d.kind);
}
let one = get_help("pipeline/nodes/web.response").expect("one node");
assert!(one.contains("| `--file` |"));
}
#[test]
fn every_definition_marker_names_a_real_node() {
let defs = official_node_definitions_for_help();
let marker = regex::Regex::new(r"<!-- node-flags:([a-z0-9_.]+) -->").unwrap();
let mut failures = Vec::new();
for page in HELP.iter() {
for caps in marker.captures_iter(page.content) {
if !defs.iter().any(|d| crate::pipeline::nodes::kind_query_matches_def(d, &caps[1])) {
failures.push(format!("{}: <!-- node-flags:{} -->", page.path, &caps[1]));
}
}
let expanded = expand_definition_markers(page.content);
assert!(!expanded.contains("— unknown node -->"), "{}: a marker rendered as unknown", page.path);
}
assert!(failures.is_empty(), "markers naming unknown nodes:\n{}", failures.join("\n"));
}
}
#[cfg(test)]
mod sekejap_markers {
use super::*;
#[test]
fn the_sekejap_page_and_skill_pin_this_engine_version() {
let version = crate::version::SEKEJAP_ENGINE_VERSION;
assert_ne!(version, "unknown", "build.rs could not read sekejap from Cargo.lock");
let pinned = format!("sekejap/blob/v{version}/docs/lang/QL_CONTRACT.md");
let page = get_help("db/sekejap").expect("db/sekejap");
let skill = expand_sekejap_markers(
&crate::platform::skills::blessed_file("zebflow-sekejap", "").expect("zebflow-sekejap skill"),
);
for (name, text) in [("db/sekejap", &page), ("zebflow-sekejap", &skill)] {
assert!(!text.contains("<!-- sekejap-"), "{name}: a marker was left unexpanded");
assert!(text.contains(&pinned), "{name}: no link pinned to v{version}");
let pinned_at = text.find(&pinned).unwrap();
let main_at = text.find("sekejap/blob/main/").expect("a link to main");
assert!(pinned_at < main_at, "{name}: the pinned grammar must come before main");
}
}
}