florecon
florecon reconciles financial ledgers as a conserving strategy algebra over a network-simplex core. You compose a matching strategy from small combinators, run it over a bag of rows, and get back groups plus the signed amount each row contributes to each group. Nothing is created and nothing is lost: what goes in equals the grouped allocations plus the leftovers, by construction — so a bad strategy yields a bad proposal, never a broken ledger.
It is a Rust crate. A domain is packaged as one self-describing WebAssembly plugin via the authoring SDK; hosts stay dumb columnar-table carriers.
Money is i64 minor units (cents), so netting is exact.
The algebra
A Strategy maps a bag of Items to (groups, residual). Leaves form
groups; combinators arrange leaves. Every node conserves signed amount.
Leaves:
exact_1to1— pair a row with an equal-and-opposite row sharing a key.agg_net— accept a bucket (by a key) that nets to zero within aTol.signal_group— group rows sharing a free-text token that net to zero.flow— a min-cost-flow arbiter (theengine): pairs opposite-sign rows by proximity in an ordering and a cost model, splitting a row across counterparties when needed. The domain is described by aFlowSpecof closures (penalty, window, block key, match keys, pair cost).soak_all/soak_small/soak_if— sweep leftovers into variance or write-off buckets.
Combinators:
seq— run steps in order; each sees only the previous step's leftovers.partition_by/partition_by_with— shard by a key and run a sub-strategy per shard (e.g. per company pair, per currency).when/identity— route rows through a guarded sub-strategy, else pass on.windowed— restrict matching to a sliding window over an ordering.pivot— run a sub-strategy in a different amount lane, translating back.fixed_point— repeat a sub-strategy on its own leftovers until it converges.
Predicates, keys, costs and orders are plain Rust closures, not a serialized expression language.
use *;
// Per company-pair, per currency: net clean buckets, pair exacts, bridge on
// references, then let the flow engine arbitrate the remainder.
let strategy = partition_by;
The workspace
Recon is the stateful facade: stream rows in, solve, sign off what you trust.
A group lives on two orthogonal axes — a lifecycle (Proposed, owned by the
solver and recomputed each solve, vs Pinned, a human decision kept verbatim)
and a provenance label. The operations decompose into four orthogonal families:
ledger upsert(id, item) · remove(&ids)
machine solve()
lifecycle pin(id) · pin_where(pred) · unpin(id)
partition merge(allocs, label, reason) · detach(id, ids) · dissolve(id)
solve recomputes the proposed pool from items − pinned mass; pinned groups
keep stable ids. merge asserts a pinned group over exact allocation amounts
(atomic). pin_where(pred) is the one bulk-pin primitive — "pin every clean
match", "pin these singletons", and more are just predicates. Conservation is
verified at the solve boundary.
The result
report() returns an allocation hypergraph:
groups—group_id,status(live/frozen),net,size,origin,reason.allocations— one row's signed contribution to one group:id,group_id,amount.
A row may contribute to several groups, so the allocations are the source of
truth and a single row-to-group assignment is a projection you choose:
Report::strict_assignments (refuses split rows) or connected_components
(settlement clusters).
Authoring a plugin
Implement sdk::Plugin — declare the raw input columns, the stable row id, the
projection to a typed row, the conserved numeraire, and the strategy — then
export_plugin! emits a self-describing wasm module. The declared schema is
enforced at ingest: a missing or mistyped column fails loudly, and a
RowView access to an undeclared column panics rather than silently reading
zero. The conformance kit (sdk::conformance::assert_conformance) mechanically
proves identity/derivation/warm-start integrity from one Arrow batch.
Authoring is meant to be LLM-assisted Rust: the closures are the strategy language (full expressivity, and the compiler is your correctness oracle), so there is no separate DSL. You never clone this repo — the author CLI ships with the host, so from your own data project:
The journey then has two phases:
-
Author (Rust only, fast). Iterate the strategy natively against a CSV sample — no wasm, no Python. The expensive solver lives in the
florecondependency (built once at-O3, then cached), so your strategy recompiles in under a second and runs at near-release speed:Edit the four marked spots in
solver/src/lib.rs, re-run, read the report and the conservation line. Repeat. -
Ship + consume (wasm + Python). When the strategy fits, build the production wasm and run it where the data already lives:
&&
florecon author / ship / check are thin, cross-platform cargo wrappers
(they need a Rust toolchain). The worked starter — the exact thing florecon new
scaffolds — is examples/starter-plugin (its README
walks the full path); plugins/interco is the larger real example (intercompany
reconciliation).
Developing florecon itself
The author CLI (florecon._cli) lives in hosts/python; the starter it ships
is bundled from examples/starter-plugin by scripts/sync-template.py (CI runs
it with --check). The engine::Snapshot (behind the serde feature) persists
a warm basis.
Design docs
docs/arch/strategy-surface.md— the algebra.docs/arch/recon-surface.md— the workspace surface.docs/arch/sdk-surface.md— the plugin SDK and wire.docs/arch/plugin-sdk.md— the original architecture shift (historical).