8. Composition patterns
This chapter is the decision space: when X applies, prefer Y, with the reasoning grounded in the trade-offs the previous chapters introduced. The patterns here are extracted from the platform’s v1 surface (the kinase notebook + the dock-assay WASM demo); as more comorphisms land, more patterns will join them.
Note on maturity. v1 has three comorphisms in the kinase notebook plus the dock-assay WASM trio. That’s enough material for the basic shape decisions in §7.1–§7.5; the multi-step chain pattern in §7.6 is projected from the architecture, not yet exercised end-to-end. Treat this chapter as a working set of recommendations that will tighten as more compositions land.
8.1. Sharing a payload vs. declaring a converter
Use a shared payload language when multiple institutions naturally operate on the same kind of data (numerical expressions, planning trees, logical clauses). The cost is up-front coordination — every consuming institution has to agree on the encoding — and the saving is structural: O(N) boundaries instead of O(N²) per-bridge converters, and identity comorphisms become possible (chapter 3 §3.3).
Use a per-bridge converter when institutions genuinely operate on different shapes — one stream-based, one record-based, one with explicit error sentinels and another with total functions. Forcing a shared payload in this case produces an awkwardly-general type that nobody finds natural, and every institution writes ad-hoc adapters from the shared shape into its preferred form.
Three signals you’re in shared-payload territory:
- The institutions you want to bridge already have a common ancestor in the literature (e.g. anything that consumes “an algebraic expression over real numbers” lives downstream of a EigenTT-shaped term language).
- Cross-institution comorphisms are easy to imagine (S → I, S → J, I → J, etc. — the combinatorics matter).
- The shared shape has a natural home in a peer namespace — not under
any one institution’s IRI.
urn:eigenius:formulas:is the v1 example; a hypotheticalurn:eigenius:planning:would be the next.
Three signals you’re not:
- The “shared” type carries fields only one consumer cares about — every other institution just sets them to default values.
- Bridges keep growing custom validation rules that try to constrain the shared shape down to what each institution actually expects.
- Adding a new institution requires extending the shared type, and existing consumers either ignore the extension or break.
8.2. Identity vs. structural comorphism
Identity comorphism when both endpoints share a payload language. The
transformation Component is Lam(t. Var(t)); the chain bytes flow through
unchanged; runtime cost is one β-step. The payoff is exact: true — the
comorphism preserves every sentence’s truth, and the audit trail says so.
The kinase notebook’s symbolics_to_intervals and symbolics_to_jump are
both identity comorphisms (modulo framing wrappers — the latter wraps a
SymbolicExpression in a SymbolicsToJuMPInput composite to carry JuMP-side
metadata; the FormulaTerm payload itself flows through unchanged).
Structural comorphism when the transformation does real work. Catalyst
→ DiffEq is the worked example: compiling a reaction network into an ODE
right-hand side is a real translation that captures the deterministic
limit of the chemical kinetics. The transformation is honest about what
it loses (the stochastic structure of the master equation), which is what
exact: false declares.
The right choice is rarely a free hand. If both endpoints already speak the same payload, identity is cheap and honest — there’s no structural argument for adding work. If they don’t, identity is a lie — you’d need to either coerce one side’s data into the other’s ad-hoc, or decline the bridge. Structural is the right answer for genuinely different shapes.
A useful question to ask: would two domain experts argue about the right way to do the translation? If yes, structural. If no, identity.
8.3. AutoOnLoad vs. OnDemand
AutoOnLoad when the claim is the result you want to record. Every commit of the gated class fires the gate; the Verdict + RuntimeInvocation commit alongside; the chain has a permanent record. Right for:
- Claims you author and want verified —
OdeSolutionclaiming a specific final state,OptimisesToclaiming a specific optimum. - Constraints you want enforced —
BoundedByclaiming a value lies in an interval; rejection onFailskeeps the chain consistent. - Audit requirements — every chain entry of a gated class has a Verdict on the chain proving the institution agreed (or didn’t).
OnDemand FIBER when you want to probe without committing. The
FIBER call queries the institution; the response lives in the transient
overlay (or, with INTO, commits at a caller-named IRI). Right for:
- Exploratory queries — “what does Symbolics simplify this to?”,
without polluting the chain with intermediate
SimplifiesToclaims. - Computed properties for use within a single query — FIBER produces a result; subsequent clauses match against it; the response evaporates when the query returns.
- Generating a candidate to commit later — call OnDemand to compute, inspect the result, decide whether to commit a claim built around it.
The middle case is OnDemand FIBER ... INTO. Same call as plain
OnDemand, but the response commits to the chain at the named IRI. Use
INTO when:
- The OnDemand call’s result is interesting in its own right, not just as an intermediate.
- Downstream queries (or cells, or programs) need to find the result by IRI rather than re-executing the call.
- You want the gate cascade — committing the response triggers any AutoOnLoad gate bound to its class.
Common mistake: using AutoOnLoad to gate an exploratory commit
(e.g. “I want to see what the institution thinks of this hypothetical
claim before committing it”). The gate’s Fails aborts the commit, so
the chain has no record of the exploration. OnDemand FIBER is the right
tool — call it, look at the response, then decide whether to commit a
claim afterwards.
8.4. Chain reinsertion vs. transient overlay
EigenQL FIBER ... AS ?var has two flavours: with INTO (chain
reinsertion) and without (transient overlay). The difference matters
when a downstream consumer wants to find the response outside the
current query’s scope.
Use INTO (chain reinsertion) when:
- The produced resource is an interesting chain entity in its own right.
- Gates downstream of it should fire (the AutoOnLoad cascade pattern).
- The IRI matters — for stable references, external system keys, or cross-cell lookups in a notebook.
- Multiple queries need to read the same response without re-executing.
Use overlay-only (no INTO) when:
- The response is intermediate machinery — a value the current query consumes once and discards.
- The institution call is read-only and side-effect-free; commits would be noise.
- Repeated runs of the same query should not accumulate chain residents.
A subtle case: when an OnDemand call is deterministic (same input,
same output), INTO is safe to use for caching — re-running the cell
finds the existing committed resource and skips re-execution. When it’s
non-deterministic (e.g. uses a random seed; depends on host BLAS
behaviour), INTO overwrites or errors depending on validator state.
The kinase notebook’s comorphism dispatches are deterministic (EigenTT
evaluation + Pkg-pinned Julia), so INTO is safe. The kinase
notebook’s hypothetical Monte-Carlo extensions wouldn’t be.
8.5. Decidable predicates as constraints
The “constraint attached to a property” pattern from
ESL §9.6:
a Decidable QueryClass IRI carried as a constraint on a property fires
during type-check reduction (Check mode) and rejects the program if the
predicate Fails.
Use it when:
- A program’s correctness depends on a value satisfying a domain-specific invariant (e.g. “the optimised Kᵢ must be within the screened range of the compound” — a treasury covenant, a regulatory bound).
- The constraint is checkable by an institution that already exists; you don’t want to duplicate the check inside the program.
- You want type-check time enforcement, not runtime — the constraint rejects the program at compile time if it provably fails.
Composes well with chain reinsertion: a comorphism’s reified output is type-checked against the chain’s constraints exactly the way a hand-authored resource would be. So you can layer:
- ESL program invokes a comorphism (chain-reinserts the produced resource).
- The produced resource has a Decidable constraint on one of its properties.
- The constraint’s QueryClass dispatches at commit time; if
Holds, the commit goes through; ifFails, the commit is rejected and the program reports the violation.
The shape generalises to multi-step pipelines: each step’s output participates in the next step’s constraint check. The chain’s role as the source of truth means each step sees a fully-validated upstream.
8.6. Multi-step comorphism chains
When you want a sequence of translations — Catalyst → DiffEq → IntervalArithmetic, or Symbolics → JuMP → Solver Verdict — there are two shapes to choose between:
Composite comorphism. Author one new comorphism whose transformation
is the composition of the two intermediate transformations. Pros: one
chain commit; one user-facing dispatch; intermediate state is opaque
(might be desirable for consumers who don’t care about it). Cons: harder
to inspect and audit (the intermediate value isn’t on the chain); harder
to reuse pieces (can’t extract just the first stage); requires authoring
the composite Component if either intermediate is structural.
Chained comorphisms (with chain reinsertion). Author one ESL program that invokes the first comorphism, captures its reify output, then invokes the second comorphism on that output. Pros: each intermediate is a chain resident with full audit (Trace::Comorphism per step); each intermediate can be inspected, queried, and gated independently; pieces are reusable across different chained pipelines. Cons: N chain commits for N steps; more chain noise; the intermediate IRIs (content-hash) need to be navigated by downstream consumers.
Trade-off principle: chained comorphisms favour transparency and reusability; composite comorphisms favour brevity and opacity. Most new pipelines should start chained — you can always collapse to a composite later when the intermediate becomes uninteresting.
A worked-shape example (not yet exercised):
program nb:fit_with_bounds : symbolics:SymbolicsToJuMPInput -> tighter:KiBoundedAbove{ // Step 1: fit the optimum. let optimum : jump:OptimisesTo = comorphisms:symbolics_to_jump(input) |> run_solver; // Step 2: bound the result. comorphisms:optimum_to_bounded_ki(optimum)}Each step’s output reinserts at a content-hash IRI; the program’s
return value is the second step’s reify output; the audit trail walks
back through both Trace::Comorphism events. The chain’s identity
property (chapter 5 §5.7) means re-running the program with the same
input dedupes both intermediate commits and the final commit.
8.7. Anti-patterns
Three common mistakes worth flagging:
Sharing a payload that “almost” matches
Trying to fit two institutions into a shared payload by extending the
shared type with optional fields that only one cares about. Symptom:
the shared type accumulates Option<...> fields, with each consumer
ignoring the ones it doesn’t recognise. The result is a payload language
that’s lying about the structure — every consumer needs runtime checks
that the fields it cares about are present.
Fix: declare a per-bridge converter. The two institutions don’t genuinely share a payload; pretending they do hides the work in defensive runtime checks.
Using AutoOnLoad to gate everything
Putting an AutoOnLoad gate on every interesting class because “audit is
good.” Symptom: every chain commit fires N gates; each commit becomes
slow; many gates produce Undecidable because the institution can’t
actually decide most claims it sees.
Fix: AutoOnLoad is for claims you want verified. If the institution doesn’t have a meaningful answer for a class, don’t gate the class. OnDemand is the right shape for “answer if asked.”
Comorphism without exact consideration
Authoring a comorphism with exact: true because “it preserves the
data, mostly.” Symptom: downstream audit code can’t distinguish a truly
exact comorphism from a “mostly exact” one; verdicts that travel
through lossy chains are reported as authoritative.
Fix: exact: true is a substantive claim. If the comorphism does
any structural collapse — coerces representation choices, drops
metadata, projects to a smaller domain — set exact: false and let
downstream code decide whether the loss matters for its purpose.
Chain reinsertion without an audit consumer
Using FIBER ... INTO to commit comorphism outputs that no downstream
code ever reads. Symptom: chain bloat with content-hash IRIs nothing
references.
Fix: if the response is exploratory, drop INTO — the overlay form
is the right tool. Reserve chain reinsertion for resources downstream
queries actually look up.