Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
8586089
feat(wit)!: ffrwd:av 0.19.0 is the node world, and ten recipes name i…
imbcmdth Oct 1, 2026
ad260f8
chore(wit): the world bump's other two moves, and the 0.18.0 bindings…
imbcmdth Oct 1, 2026
2add1a9
feat(compiler): node declarations, shape(params, bound), ports and st…
imbcmdth Oct 2, 2026
6a6a616
test(modules): the nine node stand-ins the cookbook's node recipes name
imbcmdth Oct 2, 2026
c2deeb4
Merge branch 'node-world-sdk' into node-world-cli
imbcmdth Oct 2, 2026
e3e687e
feat(compiler): node networks lower to the sidecar's node spelling, w…
imbcmdth Oct 2, 2026
cfa2128
feat(sidecar): bind the node world, and run every older world through it
imbcmdth Oct 2, 2026
c93ea8d
Merge branch 'node-world-sdk' into node-world
imbcmdth Oct 2, 2026
203fdac
feat(compiler): a node region hands one process every stream it reads…
imbcmdth Oct 2, 2026
517798b
feat(sidecar): node networks run: lanes, one NUT per edge, the node s…
imbcmdth Oct 2, 2026
68edb52
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 2, 2026
ace61b4
feat(init): --rust scaffolds a node on ffrwd-node
imbcmdth Oct 2, 2026
ee1ef7c
feat: a node is told its picture's colour, rows under a pinned track …
imbcmdth Oct 2, 2026
1b2e380
feat(compiler): rows read through a reducer carry what it adds, and o…
imbcmdth Oct 2, 2026
2eae04a
feat(sidecar): live inputs: hold pairing, feeds by port, both anchors
imbcmdth Oct 2, 2026
8dbedc8
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 2, 2026
1c06393
fix(sidecar): the listener's stream carries the pad fields the merge …
imbcmdth Oct 2, 2026
27365d8
test(sidecar): the pad spec test names its colour field
imbcmdth Oct 2, 2026
a7f92b6
feat(compiler): a node read in FROM, a NULL stream on an optional por…
imbcmdth Oct 2, 2026
7d52e5b
feat(compiler): a lateral feeds a node's held inputs, a stream's tags…
imbcmdth Oct 2, 2026
c5c751a
feat(compiler): a node at COPY's TO, its rows, -copyts for a node sou…
imbcmdth Oct 2, 2026
4cb8632
feat(compiler): a param whose schema types include array takes a list…
imbcmdth Oct 2, 2026
a71598f
feat(sidecar): the findings of the first migrations, one scheduler, t…
imbcmdth Oct 2, 2026
0740b29
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 2, 2026
a5d9e64
feat(compiler): a region writes its data to each reader, a lateral's …
imbcmdth Oct 2, 2026
2c73abf
feat(sidecar): a feed connection may carry a subset of its group, and…
imbcmdth Oct 2, 2026
2a56bc0
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 2, 2026
f4e3ece
feat(sidecar): leaky takes a coded stream
imbcmdth Oct 2, 2026
e27ab12
feat(wit): ffrwd:av 0.19.1, six additions
imbcmdth Oct 2, 2026
a2e08a6
feat(wit): ffrwd:av 0.19.1, six additions
imbcmdth Oct 2, 2026
739af62
feat(wit): a bound stream carries the hint its shape was asked with
imbcmdth Oct 2, 2026
654aeda
feat(wit): a bound stream carries the hint its shape was asked with
imbcmdth Oct 2, 2026
097e90b
feat(sidecar): host ffrwd:av 0.19.1
imbcmdth Oct 2, 2026
de80a25
feat(compiler): ffrwd:av 0.19.1, timing inputs, stream hints, re-time…
imbcmdth Oct 2, 2026
c229418
docs(sidecar): the 0.19.1 spellings a compiler emits
imbcmdth Oct 2, 2026
f6cb316
feat(sidecar): every tick says its ordinal, and the SDK modules are o…
imbcmdth Oct 2, 2026
0ca9bdf
feat(compiler): a node sink's shape is asked with what the SELECT bin…
imbcmdth Oct 2, 2026
0a400fb
feat(sidecar): ended feeds per instance, and when a start was known
imbcmdth Oct 2, 2026
6234ff5
feat(compiler): a lateral's rows ride its hold group's connection int…
imbcmdth Oct 2, 2026
e47caa5
feat(compiler): each node call carries the bound list its shape was a…
imbcmdth Oct 2, 2026
a5408a7
feat(sidecar): -bound carries the list a call's shape was asked with;…
imbcmdth Oct 2, 2026
5319a1e
test(compiler): a module called twice carries each call's -bound in c…
imbcmdth Oct 2, 2026
71d274c
feat(sidecar): -params-from takes its files in turn per name, as -bou…
imbcmdth Oct 2, 2026
7446a03
feat(sidecar): a first-frame data stream is re-stamped onto the clock
imbcmdth Oct 2, 2026
394f86b
feat(sidecar): a data input on a hold group arrives on the group's co…
imbcmdth Oct 2, 2026
655095a
feat(modules): spot and matte count sightings by ordinal and are pure
imbcmdth Oct 2, 2026
1124693
chore(modules-go): bindings for ffrwd:av 0.19.1
imbcmdth Oct 2, 2026
77e231d
fix(sidecar): a progress mark on a held picture is the input's again
imbcmdth Oct 2, 2026
8866081
chore(sidecar): ffrwd-frame 0.1.1 in the lock
imbcmdth Oct 2, 2026
042f958
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 2, 2026
81f1b69
chore: the SDK modules and the scaffold pin ffrwd-node v0.2.0 and ffr…
imbcmdth Oct 2, 2026
f1d9054
docs(examples): recipes 145 to 154 re-pinned with each node call's -b…
imbcmdth Oct 2, 2026
bc40b59
fix(sidecar): an ndjson row keeps its own text and gains pts and time…
imbcmdth Oct 2, 2026
858fe0b
fix(run): a lateral's rows writer that hosts the switch it feeds star…
imbcmdth Oct 2, 2026
90c1e90
fix(compiler): a timing input after a format takes the format it names
imbcmdth Oct 2, 2026
19c9ff9
fix(sidecar): a rate clock whose inputs all arrive unpaired ticks no …
imbcmdth Oct 2, 2026
3892edf
fix(sidecar): a stream-bound feed's end is told lead ahead, on the sa…
imbcmdth Oct 2, 2026
d37f7b9
test(sidecar): a grouped data input bound to a stream rides its group…
imbcmdth Oct 2, 2026
3b4bffd
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 2, 2026
33c0812
fix(sidecar): a rate clock with arrival inputs takes its last call on…
imbcmdth Oct 3, 2026
6400c0e
feat(sidecar): four worker threads by default instead of one per core
imbcmdth Sep 30, 2026
e550f7a
refactor(sidecar): the relay's Gate is the Cancellor
imbcmdth Oct 3, 2026
3ceecad
chore(sidecar): ffrwd-nut v0.1.5 and ffrwd-frame v0.1.1 by tag, the p…
imbcmdth Oct 3, 2026
c4b0f1a
chore(sidecar): two clippy findings under -D warnings
imbcmdth Oct 3, 2026
8545935
chore: two things CI sees that a loaded checkout did not
imbcmdth Oct 3, 2026
be0bdaa
fix(sidecar): leaky hands a picture on only once its readers have tak…
imbcmdth Oct 3, 2026
d6d3d4d
Merge branch 'node-world-cli' into node-world
imbcmdth Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 87 additions & 145 deletions cli/ffrwd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@
package segment is the directory's name unless ``--name`` says otherwise;
the namespace is ``--namespace``'s, or derived from the git remote's owner,
or required. Refuses to overwrite any file it would write. ``--rust``
scaffolds a wasm module package instead of the bare one: a cargo crate
whose ``build.rs`` finds the wit, an ``invert`` module in Rust, the lib SQL
declaring it and a recipe calling it. ``cargo build --target wasm32-wasip2
--release`` then ``ffrwd publish`` is the whole path from there.
scaffolds a node module package instead of the bare one: a cargo crate on
the ffrwd-node SDK holding a ``passthrough`` node, the lib SQL declaring it
and a recipe calling it. ``cargo build --release --target wasm32-wasip2``
then ``ffrwd publish`` is the whole path from there.
* ``search [TERM] [--json]`` -- ask the registry what it ranks for TERM and
print it, most relevant first. No term browses everything; a term matching
nothing is an empty table, exit 0.
Expand Down Expand Up @@ -191,7 +191,19 @@
from pathlib import Path
from typing import Any

from . import binaries, credentials, diagram, loudnorm, nn, redact, remote, show, store, wasm
from . import (
binaries,
credentials,
diagram,
loudnorm,
nn,
redact,
remote,
show,
store,
timing,
wasm,
)
from . import packages as packages_module
from . import publish as publish_module
from . import registry as registry_module
Expand Down Expand Up @@ -541,6 +553,11 @@ def _build_parser() -> argparse.ArgumentParser:
action="store_true",
help="render the flowchart in the terminal (needs the diagram extra)",
)
explain_view.add_argument(
"--delays",
action="store_true",
help="print each node's window and how far behind the source each output runs",
)
validate_p = subparsers.add_parser("validate", help="check that a query compiles")
_add_query_arguments(validate_p)
_add_quiet_argument(validate_p)
Expand Down Expand Up @@ -685,7 +702,7 @@ def _build_parser() -> argparse.ArgumentParser:
init_p.add_argument(
"--rust",
action="store_true",
help="scaffold a wasm module package: the Rust crate that builds it, "
help="scaffold a node module package: the Rust crate that builds it, "
"the SQL declaring it, and a recipe calling it",
)

Expand Down Expand Up @@ -1474,6 +1491,10 @@ def _cmd_explain(args: argparse.Namespace, on_warning: OnWarning) -> int:
return 1

graphs = compiled.graphs
if args.delays:
if compiled.timing is not None:
print(timing.summary(compiled.timing))
return 0
if args.mermaid or args.diagram:
text = diagram.render_diagram(graphs, compiled.plan)
if args.mermaid:
Expand All @@ -1489,8 +1510,12 @@ def _cmd_explain(args: argparse.Namespace, on_warning: OnWarning) -> int:
payload: object = graphs[0].to_dict() if len(graphs) == 1 else [
graph.to_dict() for graph in graphs
]
if compiled.plan is not None:
payload = {"graph": payload, "plan": compiled.plan.to_dict()}
if compiled.plan is not None or compiled.timing is not None:
payload = {"graph": payload}
if compiled.plan is not None:
payload["plan"] = compiled.plan.to_dict()
if compiled.timing is not None:
payload["timing"] = compiled.timing.to_dict()
print(json.dumps(payload, indent=2))
return 0

Expand Down Expand Up @@ -2413,168 +2438,82 @@ def _cmd_list(args: argparse.Namespace, on_warning: OnWarning) -> int:
) TO :'dest'
"""

# The wasm module scaffold `init --rust` writes on top of the manifest and the
# The node module scaffold `init --rust` writes on top of the manifest and the
# lockfile. One export and one recipe, both named for what the module does.
_RUST_EXPORT = "invert"
_RUST_EXPORT = "passthrough"
_RUST_EXPORT_FILE = f"src/{_RUST_EXPORT}.sql"
_RUST_RECIPE = "invert"
_RUST_RECIPE = "passthrough"
_RUST_RECIPE_FILE = f"recipes/{_RUST_RECIPE}.sql"
_RUST_BUILD_FILE = "build.rs"
_RUST_CARGO_FILE = "Cargo.toml"
_RUST_SOURCE_FILE = "src/lib.rs"
_RUST_IGNORE_FILE = store.IGNORE_NAME
_RUST_GITIGNORE_FILE = store.GITIGNORE_NAME

# The wit-bindgen the in-repo modules build with; the scaffold pins the same one.
_WIT_BINDGEN_VERSION = "0.57.1"
# The releases the scaffold's crate pins; ffrwd-node's speaks the world
# `wasm.WORLD_VERSION` names, which the manifest depends on.
_NODE_SDK_TAG = "v0.2.0"
_FRAME_TAG = "v0.1.1"

_RUST_BUILD_LINE = "cargo build --release --target wasm32-wasip2"

# Where a built module lands, and what the lib SQL therefore names.
_RUST_ARTIFACT = "target/wasm32-wasip2/release/{crate}.wasm"

_RUST_CARGO = f"""\
_RUST_CARGO = """\
[package]
name = "{{crate}}"
name = "{crate}"
version = "0.1.0"
edition = "2021"

# A wasm component: one cdylib, no binary.
[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "{_WIT_BINDGEN_VERSION}"
ffrwd-node = {{ git = "https://github.com/imbcmdth/ffrwd-node", tag = "{sdk_tag}" }}
ffrwd-frame = {{ git = "https://github.com/imbcmdth/ffrwd-frame", tag = "{frame_tag}" }}

[profile.release]
opt-level = 3
lto = true
strip = true
"""

_RUST_BUILD = '''\
// Puts the `ffrwd:av` wit where `wit_bindgen::generate!` reads it, from
// whichever of the two sources is available: FFRWD_WIT_DIR when the
// environment names one, else the `ffrwd/wasm` package installed here.
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

const WIT_DIR_ENV: &str = "FFRWD_WIT_DIR";
const WIT_PACKAGE: &str = "ffrwd/wasm";
const WIT_FILE: &str = "av.wit";

fn main() {
println!("cargo::rerun-if-env-changed={WIT_DIR_ENV}");
let source = match env::var_os(WIT_DIR_ENV) {
Some(named) => PathBuf::from(named),
None => installed_wit_dir(),
}
.join(WIT_FILE);
println!("cargo::rerun-if-changed={}", source.display());

let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let wit = manifest.join("wit");
fs::create_dir_all(&wit).expect("create wit/");
fs::copy(&source, wit.join(WIT_FILE))
.unwrap_or_else(|err| panic!("copy {}: {err}", source.display()));
}

/// The `wit` directory of the installed `ffrwd/wasm` package, asked of ffrwd.
fn installed_wit_dir() -> PathBuf {
let asked = Command::new("ffrwd")
.args(["path", WIT_PACKAGE])
.output()
.unwrap_or_else(|err| {
panic!("`ffrwd path {WIT_PACKAGE}` could not be run ({err}); set {WIT_DIR_ENV} instead")
});
if !asked.status.success() {
panic!(
"`ffrwd path {WIT_PACKAGE}` failed: {}",
String::from_utf8_lossy(&asked.stderr).trim()
);
}
let printed = String::from_utf8(asked.stdout).expect("a path, in utf-8");
PathBuf::from(printed.trim()).join("wit")
}
'''

_RUST_SOURCE = '''\
wit_bindgen::generate!({
path: "wit",
world: "video-module",
});
use ffrwd_node::{Bound, Init, Input, NoParams, Node, Out, Output, Result, Shape, Tick};

use exports::ffrwd::av::filter::{FrameInfo, Guest, Meta, Outcome, Output, StreamInfo};

struct Invert;

// JSON Schema for the `params` string a call passes; this module takes none.
const PARAMS_SCHEMA: &str = r#"{"type":"object","properties":{},"additionalProperties":false}"#;

fn validate_params(params: &str) -> Result<(), String> {
match params.trim() {
"" | "{}" => Ok(()),
other => Err(format!("invert takes no params, got: {other}")),
}
struct Passthrough {
v: u32,
}

impl Guest for Invert {
// What the module is, read before anything runs. `pixel_formats` empty
// would make this an audio module, and the two are never both.
fn describe() -> Meta {
Meta {
name: "invert".to_string(),
version: "0.1.0".to_string(),
params_schema: PARAMS_SCHEMA.to_string(),
rows_schema: String::new(),
pixel_formats: vec!["rgba".to_string()],
sample_formats: vec![],
sample_rates: vec![],
channel_counts: vec![],
rows_language: vec![],
}
}

// Once per instance, before any frame. The frame size and pixel format
// are fixed from here on, so state sized to them is built here.
fn init(
_width: u32,
_height: u32,
_pix_fmt: String,
_stream_info: StreamInfo,
params: String,
) -> Result<(), String> {
validate_params(&params)
impl Node for Passthrough {
const NAME: &'static str = "passthrough";
const VERSION: &'static str = "0.1.0";
type Params = NoParams;

fn shape(_: &NoParams, _: &Bound) -> Result<Shape> {
Ok(Shape::new()
.input(Input::video("v").clock().pixel_formats(&["rgba"]))
.output(Output::like("v"))
.pure()
.one_to_one())
}

// New parameters between frames; rejecting them leaves the old ones in force.
fn set_params(params: String) -> Result<(), String> {
validate_params(&params)
fn init(_: NoParams, init: &Init) -> Result<Passthrough> {
Ok(Passthrough {
v: init.stream("v")?.id,
})
}

// True lets the host run frames in parallel, so it must be a promise.
fn frame_independent() -> bool {
true
}

// One frame in, one frame out. `Output::Passthrough` would hand the input
// back uncopied; this one rewrites the bytes it was given.
fn process(_info: FrameInfo, frame: Vec<u8>) -> Outcome {
let mut out = frame;
let (pixels, _) = out.as_chunks_mut::<4>();
for pixel in pixels {
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
}
Outcome {
output: Output::Frame(out),
rows: vec![],
}
fn process(&mut self, tick: &Tick, out: &mut Out) -> Result<()> {
let Some(frame) = tick.frame(self.v) else {
return Ok(());
};
// Your work goes here: `tick.fetch` reads the picture, `out.frame` sends a new one.
Ok(out.pass("v", self.v, &frame)?)
}
}

export!(Invert);
ffrwd_node::export!(Passthrough);
'''

_RUST_EXPORT_SQL = """\
Expand All @@ -2585,7 +2524,7 @@ def _cmd_list(args: argparse.Namespace, on_warning: OnWarning) -> int:
"""

_RUST_RECIPE_QUERY = """\
-- Invert a file's picture, its audio carried through untouched.
-- Run a file's picture through the module, its audio carried through untouched.
-- variables: source (input media path), dest (output path)
-- example: ffrwd run {recipe} -v source=in.mp4 -v dest=out.mp4
COPY (
Expand All @@ -2601,31 +2540,36 @@ def _cmd_list(args: argparse.Namespace, on_warning: OnWarning) -> int:
# Build input, and the output it produces. The one wasm the lib SQL names
# ships out of target/ regardless; nothing else here belongs in the archive.
Cargo.toml
build.rs
src/*.rs
target/
wit/
"""

# `wit/` is written by build.rs, not by hand.
_RUST_GITIGNORE = """\
target/
wit/
"""

_RUST_README = """\
# {name}
_RUST_README = f"""\
# {{name}}

TODO: what this package does, and how a query calls it.

Written on the [ffrwd-node](https://github.com/imbcmdth/ffrwd-node) SDK, whose
README is the reference for what a node declares and emits.

```
{_RUST_BUILD_LINE}
ffrwd run {_RUST_RECIPE} -v source=in.mp4 -v dest=out.mp4
```
"""


def _rust_scaffold(name: str) -> dict[str, str]:
"""The module scaffold's files, keyed by their path under the project."""
segment = name.partition("/")[2]
return {
_RUST_CARGO_FILE: _RUST_CARGO.format(crate=segment),
_RUST_BUILD_FILE: _RUST_BUILD,
_RUST_CARGO_FILE: _RUST_CARGO.format(
crate=segment, sdk_tag=_NODE_SDK_TAG, frame_tag=_FRAME_TAG
),
_RUST_SOURCE_FILE: _RUST_SOURCE,
_RUST_EXPORT_FILE: _RUST_EXPORT_SQL.format(
export=_RUST_EXPORT, artifact=_RUST_ARTIFACT.format(crate=segment)
Expand All @@ -2643,7 +2587,6 @@ def _rust_scaffold(name: str) -> dict[str, str]:
# refuses to overwrite, checked before a name is worked out.
_RUST_PATHS = (
_RUST_CARGO_FILE,
_RUST_BUILD_FILE,
_RUST_SOURCE_FILE,
_RUST_EXPORT_FILE,
_RUST_RECIPE_FILE,
Expand Down Expand Up @@ -2794,7 +2737,7 @@ def _cmd_init(args: argparse.Namespace, on_warning: OnWarning) -> int:
"""Write the files a project starts as, into the working directory.

The manifest, an empty lockfile and a starter recipe; ``--rust`` writes a
wasm module package instead of the bare one -- the crate that builds the
node module package instead of the bare one -- the crate that builds the
module, the SQL declaring it, and a recipe calling it.
"""
directory = Path.cwd()
Expand Down Expand Up @@ -2872,8 +2815,7 @@ def _cmd_init(args: argparse.Namespace, on_warning: OnWarning) -> int:
f"it calls its functions as {name.replace('/', '.')}.name()"
)
if rust:
print(f"take the wit the module builds against: ffrwd install {wasm.WIT_PACKAGE}")
print("build the module: cargo build --target wasm32-wasip2 --release")
print(f"build the module: {_RUST_BUILD_LINE}")
print(
f"then run the recipe: ffrwd run {_RUST_RECIPE} "
f"-v source=in.mp4 -v dest=out.mp4"
Expand Down
Loading
Loading