One Shape Across the Eignex Stack

Three months on from the last status update, three posts shipped, and the Eignex rewrite, splitting one experimentation library into focused pieces, has actually moved. This is a quick checkpoint: where the libraries stand, what the site has grown into, and what comes next.

How the stack converged

The big shift since February is that kumulant and (soon) combo have converged on a single API shape for declaring computation graphs and config, both backed by skema. The same definition compiles as a typed Kotlin singleton object and serializes to a YAML or JSON document a service can author by hand or POST over HTTP.

In practice, the in-process Kotlin caller is the rare path. Cloud-deployed YAML and HTTP payloads are the expected one. Designing the typed schema and the wire format separately would have left two sources of truth and a translation layer between them. Designing one shape that does both forced the rest of the architecture into something simpler than I had expected. The previous post, From Stringly to Strongly Typed, walked through how the design landed.

Convergence happened repo by repo, not all at once. kumulant moved first; rewriting its closure-based transform graph into AST nodes was the work that exposed the pattern in the first place. Combo is still pending, but the shape it has to land on is no longer in question. klause is the deliberate holdout: its constraint AST already has its own established wire format, and the trade-offs there are different enough that folding it onto skema would be more cost than benefit. What follows is what each library looks like today.

What changed in each repo

Combo is the integration point: a policy will declare its parameter space as a skema schema, lay klause constraints over the variables, and feed observations through a kumulant aggregator that scores live variants. None of those wires is new on its own; what’s new is that they finally line up against the same variable names and the same wire format.

combo bandit policy + PGBM klause constraints between variables kumulant online accumulators + monitoring skema typed schema + wire format
How the pieces stack inside combo.

skema is a meta-library: a tool for people writing other libraries whose users declare typed schemas that have to serialize over the wire. It was cut to its own repo at 0.1.0. Two additions worth noting: SchemaDef.diff() for drift detection between two versions of a schema, and composition operators (+ and namespaced()) for plugin-style assembly of larger schemas from smaller ones. The design rationale lives in From Stringly to Strongly Typed.

klause is a constraint solver for problems that mix Boolean and integer variables, pulled out of combo, the contextual-bandit library, at the start of the rewrite. Its job inside combo is to carry the constraints the bandit has to respect when it picks variants, ruling out invalid combinations before the optimiser ever sees them. The headline differences from what combo originally shipped: CSP-style integer domains now sit alongside Booleans in a single problem, a Z3 backend handles direct SMT, and a LogicNG adapter bit-blasts the integer side to CNF and hands off to a real SAT solver. The default LocalSearchSolver runs simulated annealing on the local-search-friendly subset, and a brute-force backend cross-checks all three on small instances to make sure the heuristic and exact solvers agree across every case it can enumerate.

kumulant is a streaming statistics library that does two jobs inside the rewrite: it backs combo’s probabilistic gradient boosting machine with online accumulators, and it provides the monitoring layer for the cloud-deployed combo service. The accumulators themselves (means, sums, decaying windows, and probabilistic sketches like TDigest, ReservoirHistogram, and SpaceSaving) compose into a typed schema and update one value at a time. The big shift this cycle was the operation graph above them: it now lives as AST-typed VectorExpr, ScalarExpr, and BoolExpr nodes, which brought kumulant onto skema’s wire-friendly shape. Transforms and reductions are now data, not closures, so a config can be authored as YAML and shipped through a deployment pipeline without a recompile.

The site itself

I’ve also started crossposting: each post now goes out as a trimmed and retitled copy on dev.to, Hashnode, and Medium. I don’t enjoy it, and the rewriting needed to keep search engines pointing at the canonical here isn’t free, but a personal blog with no incoming links has to be discoverable somehow. Mastodon and Bluesky links sit in the footer next to GitHub.

Beyond syndication, the rest of the site has had a lot of polish along the way too. Images are zoomable on click, the icon set has grown a fair bit, and the front-page splash finally lays out cleanly across the horizontal/vertical and desktop/mobile combinations that had been giving me grief for months. I should probably stop caring this much about trivial details, but here we are.

What’s next

Combo itself is the next domino. The plan is to reattach the learning side, GLM, random forest, and probabilistic gradient boosting, on top of a skema-typed schema, and serve it behind an HTTP boundary that takes a YAML-serializable config. With kumulant already on the same shape, combo is the second tenant of one design rather than its own.

After combo, eignex.io finally gets something on it. Today it’s an empty placeholder while this blog lives at eignex.com; the plan is for eignex.io to come online as the product surface, a managed server with a UI fed by the same configs the libraries already speak. That is still some distance off, but the path is no longer guesswork. Concretely, the next milestones are an end-to-end example pinning a klause schema, a kumulant aggregator, and a combo policy together against a small synthetic A/B problem, and a 0.1.0 release of skema published to Maven Central so the rest of the stack stops depending on a local mavenLocal install.

A personal note. I’m on part-time parental leave with twin babies, which is the honest reason the cadence is what it is. Writing happens in the gaps between bottles and naps. The slower pace has been good for the design work, even if it means the next post might be another few months out.