bnto-core 0.18.1

Core WASM engine library for Bnto — shared types, traits, and orchestration
Documentation

bnto-core

Foundation crate for the bnto engine. Types, traits, pipeline executor, and node registry.

Overview

bnto-core defines the contracts that all node crates implement and the executor that runs pipelines. It has no WASM dependencies. It compiles to native Rust for tests and CLI, or links into the bnto-wasm cdylib for browser execution.

Every other engine crate depends on bnto-core.

Key Concepts

NodeProcessor Trait

Every node implements this trait:

pub trait NodeProcessor: Send + Sync {
    fn metadata(&self) -> NodeMetadata;
    fn process(&self, input: NodeInput) -> Result<NodeOutput, BntoError>;
}
  • metadata() - self-describes the processor: type, operation, accepted formats, parameters with constraints
  • process() - transforms input bytes into output bytes. Stateless; all config comes via the params map in NodeInput

NodeRegistry

Maps compound keys ("nodeType:operation") to Box<dyn NodeProcessor>. The executor looks up processors by key at runtime.

registry.register("image", "compress", Box::new(CompressImages));
let processor = registry.get("image", "compress")?;

Pipeline Executor

execute_pipeline() is the main entry point. It:

  1. Parses the PipelineDefinition (deserialized from .bnto.json)
  2. Walks nodes in dependency order
  3. Chains output from one node as input to the next
  4. Handles container semantics (loop iterates per-file, group sequences children, parallel fans out)
  5. Emits PipelineEvents for progress reporting
  6. After a node fails, skips the rest of its chain except closing steps (nodes marked "always": true), which run on the files the failed step handed over (BntoError::with_output); the run still fails with the first error, and PipelineFailed comes once, last. See .claude/strategy/engine-execution.md#closing-steps-always

Recipe Check

check_pipeline() checks a PipelineDefinition against a registry without running it, walking the nodes as the executor does (I/O nodes skipped, container children entered). Each CheckFinding names the node, its type and, where one is at fault, the param. It reports an unknown node type, each message the node's validate() returns, and a malformed placeholder: an unclosed {{, or a {{namespace. outside fields, env, ctx, node, item and vars. It also reports always on an I/O node, where it has no effect, and every edge problem at every level (the root and each container's children): a cycle (named as a → b → a), an edge to an unknown id or one at another level, a duplicate edge, an edge into input or out of output, and a node no edge touches. These come from graph::level_errors(), which lists every error level_flow() would refuse with (level_flow returns the first), so check and run never disagree. Each is reported on the node at fault and names the edge. Placeholders are checked for form only, so no vars or inputs are needed. It returns a CheckReport: the findings, plus run_time, a RunTimeParam per param left for bnto run -p to give. A param the recipe leaves out is needed when validate() refuses without it and stops refusing once it has a value; it is left for run time when -p can give it, the way the CLI applies one: the node's id is non-empty without : or =, no earlier node (searching each container's children before the next node) has that id, the node declares no field of that name, and the param is not an object (-p gives text, numbers or true/false). A needed param that fails any of these is a finding saying why no run can give it.

PipelineEvent

Structured progress events consumed by the JS layer:

  • PipelineStarted { total_nodes, total_files }
  • NodeStarted { node_id, node_type, operation }
  • FileProgress { node_id, file_index, total_files, file_name }
  • NodeProgress { node_id, done, total, message } — a node's own live report, throttled; total is None when the node can't know it (nodes call ProgressReporter::report or report_count)
  • NodeCompleted { node_id, duration_ms }
  • PipelineCompleted { total_duration_ms }
  • PipelineError { node_id, message }

ParameterDef

Self-describing parameter metadata used by the TypeScript codegen to generate UI controls:

  • Name, type, default value, constraints (min/max/step/options)
  • Conditional visibility (visible_when - show param only when another param has a specific value)
  • Surfaceability flag (whether the param appears in the config panel)

Directory Structure

src/
├── lib.rs                # Public API: re-exports all modules
├── errors.rs             # BntoError enum (thiserror)
├── events.rs             # PipelineEvent tagged union for progress reporting
├── metadata.rs           # NodeMetadata, ParameterDef, Constraints, NodeCategory
├── processor.rs          # NodeProcessor trait, NodeInput, NodeOutput
├── progress.rs           # ProgressReporter (target-agnostic closures): percent, count, command output
├── progress_throttle.rs  # Clock-injected throttle for live progress
├── registry.rs           # NodeRegistry: compound key dispatch
├── pipeline.rs           # PipelineDefinition, PipelineNode, PipelineFile
├── pipeline_edge.rs      # PipelineEdge: a connection between two sibling nodes
├── pipeline_settings.rs  # PipelineSettings: iteration mode and the run's time limit (`timeout`)
├── graph/                # level_flow(): validate one level's edges, Kahn order, file routing (not yet used by the executor)
├── run_summary.rs        # RunSummary (serializable run outcome) + NodeTally (per-node counts from events)
├── definition.rs         # Document-shape types for .bnto.json (authoring view)
├── definition_schema.rs  # JSON Schema generator for .bnto.json
├── slash_path.rs         # slash_path(): relative path -> `/`-joined output name on every OS
├── time.rs               # rfc3339_utc() / parse_rfc3339_utc(): epoch ms <-> RFC 3339 UTC
├── control/              # Control processors: assert (fail the run when a comparison does not hold), report (print a templated line)
├── data/                 # Data processors: edit-fields (set item fields from templates over the others), summarize (one item per group, with its count and field totals)
├── check/                # check_pipeline(): unknown types, validate() refusals, placeholder form, edges, params left for run time
└── executor/
    ├── mod.rs            # Public API + node dispatch logic
    ├── chain.rs          # Node chaining: each output feeds the next
    ├── closing.rs        # After a failure: skip nodes, run `always` closing steps
    ├── primitive.rs      # Leaf node execution
    ├── container.rs      # Container execution (loop, group, parallel)
    ├── time_limit.rs     # The run's time limit, checked before each node, file and iteration
    └── tests/            # 13 test modules, 45+ tests

Development

# Run unit tests for this crate
cargo test -p bnto-core

# Run all workspace tests
task wasm:test:unit

# Lint
task wasm:lint

Testing

Extensive executor tests in src/executor/tests/ (13 modules, 45+ test functions):

  • basic.rs - simple pipeline execution
  • containers.rs - loop/group/parallel node semantics
  • errors.rs - error propagation and recovery
  • metadata.rs - processor self-description
  • progress.rs - event emission ordering
  • recipes.rs - full recipe integration tests
  • sub_pipelines.rs - nested pipeline execution