We'd love your help. Say hi on Discord, open a discussion, or comment on any issue you fancy.
You need the Rust toolchain (latest stable, with rustfmt and clippy) and
protoc — a transitive dependency builds proto files, so a plain workspace
build needs it:
scripts/setup-dev.sh # installs protoc; Debian/Ubuntu and macOSThen check everything works end to end:
git clone https://github.com/wingfoil-io/wingfoil.git && cd wingfoil
cargo test -p wingfoil
cargo run -p wingfoil --example hello_graphA few adapters need more (Aeron wants clang, libuuid and CMake ≥ 3.30; some
adapter tests want a live service) — CLAUDE.md has the details,
and none of it is needed to work on the engine.
Where to start: the
good first issue
label, or anything labelled size: small. Issues also carry priority: and
area labels (core, io-adapter, python) if you want to browse by
interest. Not sure whether an idea fits? Ask first in an issue or on Discord —
that is cheaper for both of us than a PR that has to be unwound.
Read first: docs/wingfoil-architecture.md
is the shape of the engine and the one decision everything else follows from.
Worth 20 minutes before your first non-trivial change.
Wingfoil is a ground-up rebuild, on the Op pattern, of the original
MutableNode engine — see README.md for the design objectives
and docs/planning/port-plan.md for the
historical account of the port.
Everything branches from and merges into main. next was the integration
branch that staged the replacement engine; it has landed and is retired, so
there is no second base branch any more.
Never commit directly to main. Cut a branch, push it, open a PR with base
main. Branch names are simple and descriptive — add-metrics,
fix-error-handling.
The most valuable contributions are:
- A new node/operator — follow the
/new-opskill (.claude/commands/new-op.mdfrom the repo root) and "Adding an op" indocs/adding-an-op.md. Most single-input ops need only anOpimpl with#[op(build = ...)]plus a 3-line fluent method; the compiled path is zero-touch. - A new I/O adapter — follow the
/new-adapterskill (.claude/commands/new-adapter.mdfrom the repo root), which encodes the layering rules (sources overchannel/poll, sinks overfor_each, extension traits out of the prelude). - Python bindings for an adapter — the
/bind-adapterskill.
- Behaviour is pinned, not re-derived. Tests assert exact values and tick times. Where an expectation was captured from the original engine it is a constant with its provenance in a comment — do not weaken it to make a change pass.
- One mechanism per op. Semantics live in one
Op::cycle— no duplicated logic per engine, no per-op tables in the macro. - Burst model. Same-instant values are delivered atomically in one
Burst; nothing is coalesced or dropped. - Fallible, with context. No
.unwrap()outside#[cfg(test)]and doc examples; propagate with?andanyhow::Contextat I/O boundaries. - No locks on the graph path. Background threads talk to the graph through the channel layer.
From the repository root (the crates are root-workspace members):
cargo build -p wingfoil
cargo test -p wingfoil --all-features
cargo bench -p wingfoil # three-tier regression gate
cargo fmt --all
cargo lint && cargo lint-all # workspace clippy aliases, mirror CIThe default feature set is empty (default = []) and dependency-free — every
adapter is behind its own feature. Run cargo lint-all before pushing:
feature-gated code is the easiest thing to break without noticing, and it is
what CI runs.
Adapter tests come in two files. tests/<name>_adapter.rs needs nothing
running and is part of the ordinary cargo test suite.
tests/<name>_integration.rs needs a real service or real sockets, and is
compiled but not run by the normal job — CI's test job filters it out
with -E 'not binary(/_integration$/)', so the _integration filename suffix
is the only thing keeping it out. Each one runs in its own workflow instead
(see .github/workflows/README.md).
To run one locally you need its feature and whatever it talks to. Every
*_integration.rs file opens with the exact command and prerequisites — read
that header first. The three shapes:
-
Docker, brought up by the test. etcd, redis, postgres, kafka, fluvio, otlp and aeron use
testcontainersand start their own container, so a running Docker daemon is the whole prerequisite:cargo test -p wingfoil \ --features redis-integration-test -- --test-threads=1 --nocaptureWithout Docker these fail with
Socket not found: /var/run/docker.sock. -
Docker, brought up by you. Prometheus scrapes a live exporter, so bring the stack up first (
docker compose -f crates/wingfoil/examples/adapters/telemetry/docker/docker-compose.yml up -d); the test skips itself with a printed notice if Prometheus is unreachable. KDB+ has no freely-licensed image at all — start aq -p 5000yourself (KDB_TEST_HOST/KDB_TEST_PORTto point elsewhere), and it likewise skips rather than fails when there is nothing there. -
No service at all, just real sockets or shared memory:
web(in-process server over loopback),zmq(needslibzmq),fix(in-process acceptor + initiator) andiceoryx2(a writable/dev/shm). These are tier-2 only because they are slow and timing-sensitive, not because they need infrastructure — the feature flag is all they want.
Because they run against a live wall clock, integration tests generally assert
values rather than exact tick times; the deterministic
HistoricalFrom(NanoTime::ZERO) value-and-tick-time assertions belong in the
_adapter.rs half.
Maintainers only, and both steps are manual dispatches — see
docs/RELEASING.md.