Repository navigation
Speculative Nucleus Facade
Speculative design. Open, not settled — thinking and proposals, not shipped behavior. See Roadmap for what's in flight.
Status: speculative (2026-10-04). Survey of what libraries take from compute-engine today, and what the nucleus would expose in its place. It backs the "only the nucleus knows compute-engine" direction in Speculative-Per-Package-Repos §4 and is meant to decide whether to adopt it, not to assume it. Counts are a snapshot of main and go stale; the shape of the table is the point.
-
The import surface is tiny. Libraries import two names almost everywhere: the
ComputeEnginetype and theBoxedExpressiontype. A handful of files import a dozen others (§2). A lint rule against direct imports is cheap, and most of the migration is one codemod. -
The real coupling is members, not imports. Everything a library does with an engine is a method on a
ComputeEngineor aBoxedExpressionit already holds:ce.number,ce.function,ce.declare,.evaluate(),.operator,.re. Re-exporting the types keeps all of that reachable. What breaks on a compute-engine minor is a short list of patterns (§3), not the import lines. -
Libraries already depend on the layer that would become the nucleus. Nearly every library imports
@enumeratio/engine, andanalyticimportsce-patchesfrom most of its files. The facade is mostly those two packages plus a few helpers fromboxesandstructures, put behind one name. -
"Almost only Epsil" is a different axis, and the facade doesn't deliver it. Libraries are mostly TypeScript kernels with a thin
declareon top. Most kernels don't import compute-engine at all, so a CE-free library is soon; an Epsil-only library is not (§5).
My read: the import boundary, the pin, and the lint rule pay for themselves. Narrowing the members (§4.3) is a second, optional step, and it costs casts. The Epsil goal rides on kernels, per head, and is orthogonal.
Libraries only (packages/symbols/{algebras,analysis,arithmetic,combinatorics,groups}), src/ and tests/ counted separately, files / packages. Class: a facade, b hook, c kernel code, d escape hatch.
| Import | Class | Library src | Library tests | Becomes |
|---|---|---|---|---|
ComputeEngine (type) |
a | many files, most packages | – | Engine |
ComputeEngine (new) |
a | a few files (statistics probe, generators) | most test files |
createEngine(), bareEngine()
|
BoxedExpression (type) |
a | many files, most packages | – | Expr |
MathJsonExpression (root, /epsil) |
a | a few files, a few packages | – | Json |
isNumber, isSymbol
|
a |
analytic only |
– | guards in the facade |
BigDecimal |
c |
analytic, modular
|
– |
numerics subpath |
LatexDictionaryEntry, Parser, Serializer (/latex-syntax) |
b |
residues, combinatorics
|
– |
LatexRule hook |
JavaScriptTarget (/compile) |
b | braid |
– | compile hook |
CollectionHandlers |
b/d | combinatorics |
– |
extendHead option |
LatexSyntax, LATEX_DICTIONARY, compile
|
b | – | two packages |
createEngine() loads them |
Nothing else. The nucleus side imports the rest (below), none of it from libraries:
| Import | Class | Used by |
|---|---|---|
parseEpsil, serializeEpsil, resolveLibraryNames, executeEpsil
|
b |
formats, engine, components
|
WGSLTarget, GLSLTarget, PythonTarget, JavaScriptTarget, compile
|
b |
formats, frontend, components, the site |
CancellationError, isFunction, isString, isDictionary
|
a |
evaluation, ce-patches
|
BoxedType, Type, version, dynamic import()
|
a/d |
frontend, components, bench
|
Not imports, but this is where the surface is. Counts are files in library src/.
| Member | Class | Reach |
|---|---|---|
ce.number, ce.function, ce.declare
|
a | every library with kernels |
ce.symbol, ce.box
|
a |
box in a few libraries, symbol in most |
ce.Zero, ce.One, ce.True, … |
a | a few libraries |
.evaluate(), .N(), .operator, .symbol, .re, .im, .json
|
a | most libraries |
.isInteger, .isFinite, .isNaN, .isZero, .isSame, .subs, .ops, .canonical
|
a | several libraries |
ce.precision, .bignumRe, BigDecimal
|
c |
analytic, combinatorics, modular, numerals
|
ce.declareType |
a | the seven algebra libraries |
ce.lookupDefinition as a "is it native?" probe |
d |
analytic, combinatorics, statistics, and one file each in several others |
mutating the looked-up definition (.operator.evaluate, .signature, .collection) |
d |
analytic, combinatorics, residues, hypercomplex, groupalgebra, number-theory
|
capturing a native handler (ce.box([…]).operatorDefinition?.evaluate) |
d |
analytic, numerals, hypercomplex
|
ce.pushScope/popScope/assume
|
d |
analytic (Refine/Assuming), combinatorics
|
x as never, as unknown as
|
d | hundreds of occurrences, most in analytic and combinatorics
|
evaluateAsync, withTimeLimit
|
a |
evaluation only |
ce.box(json as never) is the most common cast: box's parameter type rejects arbitrary MathJSON. A facade box(json: Json) removes most of them at once.
@enumeratio/engine has what libraries need most, as plain functions over a ComputeEngine:
-
Reading values out of expressions:
operandsOf,collectionElements,symbolNameOf,integerAt,bigIntegerAt,bigRationalAt,stringAt. Written when compute-engine'sisFunction()and friends didn't ship; they do now, so the two sets overlap. -
Extending a native head in place:
wrapOperator,widenSignature,threadOverLists,mayBeInteger, andoverloads. -
Options, messages, deadlines, randomness, InputForm, and the
compiledentry.
It does not wrap: constructing an engine, typing box(json), the native-head probe, capturing a native handler, attaching a collection, scopes and assumptions, precision, LaTeX hooks, or compile targets.
ce-patches exposes three things at once:
- the patch registry (
applyPatch,applyPatches,declareLibrary,patchSymbols), applied from a library's owndeclare; - numeric kernels re-exported for libraries: complex arithmetic,
BigCx, balls,logGamma, Hurwitz and Lerch, Barnes G, Stieltjes, Dirichlet L, Clausen, polylog; - boxing helpers,
EvalOptions,NativeEval,wantsNumber,isFiniteNum,isRealInt,numberResult,declined,BoxInput, precision helpers, and a./wgsl-complexsubpath.
The third group is the de-facto facade analytic already programs against. Folding ce-patches into the nucleus means one package owns the second group (pure numerics) and the third (helpers), and the registry stops being something libraries call.
Three ways to extend a native head exist now: wrapOperator/widenSignature in engine, extendBuiltin in structures, and compute-engine's own declare(name, def, { extend: true }). The CE API adoption session started moving engine onto the third: branch origin/backup/ce-api-adoption-wip replaces the hand-written signature join (joinWide and its union logic) with addSignature plus extend: true, and routes wrapOperator through the same call. It is unfinished. Two things to settle before building on it: widenSignature joined signatures into one union, while addSignature adds an overload, so result types now matter (the branch had to change Partition's result from any to list<any>); and later evaluate extensions replace earlier ones, so chaining still works by capture. The facade's extendHead should be that call, not a fourth mechanism.
One package, @enumeratio/nucleus (today's engine, with ce-patches inside). Subpaths keep the main entry light: libraries that only declare heads don't load numerics or compile targets.
Main entry:
-
Types.
Engine,Expr,Json,HeadDefinition,EvaluateHandler,EvalOptions,NativeEvaluate,Type. -
Guards and accessors.
isNumber,isSymbol,isFunction,isString,isDictionary; the existing readers. Pick one set (§7, question 5). -
Building.
box(ce, json),num,fn,sym,str: typed overJson, soas nevergoes away.ce.numberandce.functionstay as methods whileEngineis compute-engine's own type (§4.3). -
Declaring.
declareHead(ce, name, def),declareType, andextendHead(ce, name, patch)overdeclare(…, { extend: true }), withwrapOperator,widenSignature,threadOverListsbuilt on it.extendHeadtakesevaluate(wrapped by capture),addSignature,collection,broadcastable,canonical. -
Probing.
isNativeHead(ce, name)for the "never redeclare" probes, andnativeEvaluate(ce, head)for capturing the handler. -
Scopes.
withAssumptions(ce, conds, fn)overpushScope/assume/popScope, restoring on throw. -
Options, messages, deadlines, randomness, overloads. As they are in
engine. -
Definitions in data. What
entryandmanifestalready own (EpsilDefinition, the notation data), re-exported where a library needs the type.
/numerics: pure kernels with no compute-engine types: complex arithmetic, BigCx, balls, the special-function kernels from ce-patches, and BigDecimal (re-exported, since compute-engine's own class is what its boxed numbers hold). The kernels stay importable by a library that wants only them.
/testing: createEngine(libraries?) and bareEngine(). Tests build engines in most test files today; this is the one place that does it, and it loads the notation and LaTeX dictionary before construction, which compute-engine fixes at that point.
/compile, /epsil: the compile targets and the Epsil reader and printer, for the toolchain and interface. Libraries have no use for them.
Interfaces a library implements instead of reaching for compute-engine's.
| Hook | Replaces | Shape |
|---|---|---|
| Notation |
registerNotation, raw LatexDictionaryEntry lists |
The existing PackageNotation (traditional rules, LaTeX, macros), compiled from notation.json per head. Already data for most libraries; the raw entries in residues and combinatorics become LatexRule. |
LatexRule |
LatexDictionaryEntry, Parser, Serializer
|
{ name, kind, trigger, precedence, parse?(p, lhs), serialize?(s, expr) } with p and s narrowed to what those two files call (parseGroup, parseToken, wrap, wrapShort, serialize, serializeFunction). |
| Formatters | box rules, makeBoxes
|
As boxes has them; the notation hook feeds them. |
| Serialization and parsing |
parseEpsil, serializeEpsil, the InputForm pass |
Nucleus internal. Libraries give records; they never call these. |
| Compile |
JavaScriptTarget, compile, the compile field on a definition |
HeadDefinition.compile as a typed field, and one compileTarget(name, impl) registration. braid's direct JavaScriptTarget use becomes a call to the nucleus's own compile. |
| Collections |
CollectionHandlers, operator.collection = …
|
extendHead(ce, name, { collection }), merged by capture like evaluate. |
| Carriers and algebras |
declareAlgebra, declareCarrier*
|
As structures has them. |
| Components | direct compute-engine use in components and frontend
|
Out of scope here, and an open question (§7, question 3). |
Two steps, in this order.
-
Boundary.
EngineandExprare aliases of compute-engine's own types. The lint rule (§4.5) and the pin do their work; nothing about what a library can do changes. This takes care of the pin (one exact compute-engine, in the nucleus only) and of upgrades being a nucleus matter, as long as the facade absorbs the §2.1 patterns. -
Narrowing.
EngineandExprbecome interfaces holding only the members libraries use (a couple of dozen across the two). That is the "controlled subset", and it is what makes a compute-engine rename invisible to libraries. It costs: a narrowedExpris no longer assignable where compute-engine wants its own, so every path back in goes through a nucleus function that casts once. Do it per library, driven by the usage table, after step 1. Skip it if step 1 plus the §2.1 absorption turns out to keep upgrades quiet.
@enumeratio/nucleus/unstable, with unstable/latex-syntax and unstable/compile beside it, mirroring compute-engine's own subpaths.
-
Shape. Re-exports compute-engine as the nucleus's pinned copy, so there is exactly one class identity in the process, and adds
raw(x): given anEngineorExpr, the underlying compute-engine value (identity at step 1; the escape from the narrowed types at step 2). Nothing else. -
Undocumented. No wiki page, no API listing, no
d.tscomments beyond a one-line warning. The semver of the nucleus follows the facade and excludes this entry. -
Accountable. A use carries a one-line comment naming why and a link to the issue or compute-engine bug it works around (
// unstable: <reason> <issue>). The toolchaincheckprints every use across libraries, and the census counts them. The target is zero; the count is how we see what the facade is missing.
-
Imports.
no-restricted-imports(oxlint, throughvp check) for@cortex-js/*in every library, static and dynamic, with an override for the nucleus's own tree. Message points at the nucleus. -
Manifest. The toolchain
check/packrejects a library whosepackage.jsonlists@cortex-js/compute-engineas a dependency or peer; the nucleus is the peer. -
Unstable. Importing
@enumeratio/nucleus/unstableis allowed only with the justification comment above (a small script, since oxlint has no rule for it). -
Ratchet. A committed baseline lists the files that still violate. The count may only go down, so the rule lands in one PR before the migration and tightens as lanes clear files. Tests are exempt until
createEnginehas landed. -
Candidates, later. Restrict
lookupDefinition,operatorDefinitionandas neverthe same way; each becomes a one-line facade call once §4.1 has them.
"CE-free" means the library imports nothing from compute-engine, only the nucleus. "Epsil-only" means its heads are records with Epsil definitions and no kernel. LOC are src/ TypeScript, rounded; glue is declare*, notation, index; the glue column includes handler code that lives in declare files, so it overstates how thin that layer is.
| Library | LOC | CE-importing src files | Beyond the facade | Kernel | CE-free | Epsil-only |
|---|---|---|---|---|---|---|
hecke, incidence, quiver
|
350–450 each | one or two | one native probe in hecke
|
exact algebra over bigint/rational | now | no; kernel is the algebra |
hopf, diagram
|
~800 | one or two | none | algebra, bases, diagrams | now | no |
geometric |
~430 | four | none | thin over hypercomplex
|
now | possibly mostly |
polytope |
~800 | none | none | geometry | already | no |
adeles |
~1,000 | one | none | exact arithmetic | now | no |
groupalgebra |
~1,100 | one | signature widening, one probe | group algebra | now | no |
braid |
~1,600 | two | compile hook | braid words, Jones, Laurent polynomials | after the compile hook | no |
modular |
~1,800 | one | BigDecimal |
PSL₂(ℤ), forms, continued fractions | after /numerics
|
no |
hypercomplex |
~1,200 | three | definition capture (InvisibleOperator) |
Clifford and hypercomplex algebra | after nativeEvaluate/extendHead
|
no |
residues |
~1,700 | four | LaTeX hook, collection, probe |
sieves, factoring, discrete logs | after LatexRule + extendHead
|
arithmetic identities, little |
numerals |
~2,100 | three | definition capture | numeral systems, adic | after nativeEvaluate
|
systems are data already; adic kernels stay |
number-theory |
~2,900 | nine | probes, handler wrapping (~2k LOC of rule handlers) | exact arithmetic, Gaussian integers | after extendHead
|
the widening rules, partly |
statistics |
~5,000 | seven | probes, in-place patching | closed-form builders over expressions; samplers | after the §2.1 patterns | pdf/cdf/CF/MGF tables, yes; samplers no |
combinatorics |
~29,000 | about fifty | probes, in-place patching, scopes, LaTeX, collection, a bare engine |
~17k LOC pure enumerators and ranking; ~10k LOC with CE | glue yes, after the same list | counts and ranks as formulas, yes; generators no |
analytic |
~19,000 | all but a handful |
BigDecimal, guards, definition capture, scopes |
see below | the largest migration by far | the smallest share |
Seven or eight libraries could be CE-free in one pass, as soon as the main-entry facade exists: they use the two types and a few helpers, and a probe or two. That's the cheap, early win, and where a codemod does the work.
analytic is the outlier: roughly 110 source files, nearly all importing compute-engine, and almost none of it for the kernel. Most of those files hold a pure numeric function and the declare wrapper in the same file, so the import is for the wrapper's types. Splitting each file into kernel and wrapper makes the kernel CE-free before anything else changes. The code falls into groups (rough share of LOC):
| Group | Share | Examples | Facade needs | Epsil outlook |
|---|---|---|---|---|
Numeric kernels (double, BigDecimal, balls) |
~40% | Hurwitz/Lerch/Barnes G (in ce-patches), Carlson, Jacobi, theta, Meijer G, hypergeometric, nsum, interval balls, correctly-rounded N, certified |
/numerics, BigDecimal
|
Stay TypeScript. Arbitrary precision and error bounds are the point; a recurrence-defined few (Chebyshev, Legendre, harmonic, q-series, Nörlund, Keiper–Li) are Epsil-sized and could move. |
Rule and special-value tables behind wrapOperator pre-checks |
~20% | elementary special values, trig normalisation, inverse compositions, infinity arguments, constant rounding, widened, closed-forms-113
|
extendHead, nativeEvaluate
|
Closest to Epsil: rows of identities with a guard. Needs a rule format whose guard can say "applies when". Medium. |
| Symbolic algorithms | ~30% |
dsolve, Laplace/Fourier/Mellin/Hankel tables, optimize-core, differential-root, difference-root, find-instance, function-properties, signals
|
scopes, lookupDefinition on Integrate/Sum/Apply, derivative tables |
The transform pair tables and the waveform definitions (signals) are data. The solvers and reducers are real code. Hard. |
Interval and Around |
~7% | interval arithmetic, tagged propagation, centered intervals | structured values | Medium. |
| Alias and delegating heads | small |
XGCD → ExtendedGCD, RisingFactorial → Pochhammer
|
none | Trivially Epsil: XGCD(a, b) := ExtendedGCD(a, b). Quick win and a good first test of the definition path. |
| Platform | small |
shader.ts (a WGSL port of the zeta kernel) |
compile hook, ./wgsl-complex
|
A component/codegen concern, not a library one. |
The same split holds in ce-patches, which is almost all numerics and patch wiring; its kernels become the /numerics subpath, and the patches become the nucleus's own business.
Every head's meaning is data and Epsil: its record, its examples and its Epsil definition. A hand-written kernel is allowed beside it as an accelerated implementation, never instead of it. The kernel is cross-verified against the Epsil definition (agreement tests over the examples, Plausible sampling toward the domain's edges), so a library can drop or replace it without changing what the head means. This keeps the facade small where it matters: kernels need numerics and the declare glue, not the full engine.
Cheapest and most shared first. Each step is one PR (fewer, bigger), and the ratchet keeps the list honest.
-
Prerequisites. Finish or abandon the extend-based branch (§3), so
extendHeadhas one meaning. Settle the name (nucleusvsengine); package renames wait (Component-Naming), so the facade can ship asenginegrowing new subpaths and be renamed later. -
Types and the lint ratchet. Add
Engine,Expr,Json, the guards and/testing'screateEngine; land the lint rule with a baseline. A codemod rewrites theComputeEngine/BoxedExpressionimports (the majority of all lines), and the same codemod swaps test-file engine construction. The §5 "now" libraries go to zero direct imports here. -
Absorb the §2.1 patterns:
box(json)(removes most casts),isNativeHead,nativeEvaluate,extendHeadwithcollection,withAssumptions. Several dozen probe sites and a few dozen in-place mutations, mostly inanalyticandcombinatorics; the mechanical ones by codemod, the rest by hand. This is the step that makes a compute-engine minor invisible. -
Hooks:
LatexRule(residues,combinatorics), the compile hook (braid), notation data for the rest./numericstakesBigDecimal(modular,analytic). -
Fold
ce-patchesin, once its helpers are the facade's.analytic's ~75 files change an import path; the patch registry goes behind engine construction instead ofapplyPatchin eachdeclare. This changes who applies a patch, which touches declare order; coordinate with Speculative-Overloads-as-Data. -
Kernel and wrapper split, library by library, smallest first: the algebras,
adeles,braid,modular,hypercomplex,residues,numerals,number-theory,statistics,combinatorics,analyticlast. Each library ends with zero direct imports and its kernels CE-free. - Narrowing (§4.3 step 2), per library, if it still looks worth it.
- Epsil, per head, alongside: aliases first, then the recurrence-defined and table-defined heads, reading from the same records the library template uses (Speculative-Vdom-Markup §4.2). This runs on its own clock.
Steps 1–3 are the whole of "libraries never import compute-engine". Steps 4–7 are how little code is left.
-
Types: start with aliases of compute-engine's own types (lint and pin only); narrow
Engine/Exprper library later, where it pays. -
The full compute-engine API stays reachable only through the non-public, opt-in
unstableentry. Whether the interface layer (frontend, components) is held to the same rule is still open. -
Tests use a common testing engine from the nucleus (
/testing,createEngine). - Kernels: see "Epsil definitions and hand-written kernels" above.
-
The extend-based rework (
wrapOperator/widenSignatureondeclare(…, { extend: true })withaddSignature) gets finished, not abandoned;extendHeadbuilds on it. -
Heads are declared once, contributed to by many. A package doesn't own a head: compute-engine or the lowest package that introduces it declares it, and any other package contributes its own signatures, overloads or overrides through
extendHead. What's forbidden is redeclaring an already-declared head (a clobber decided by order) or two packages contributing the same signature.
- Re-export or narrow? Aliases get the lint, the pin and an easier bump, and leave the full member surface reachable. Narrowing gets the controlled subset and costs a cast at each boundary. Start with aliases and decide per library?
-
Numerics. Does
/numericslive in the nucleus, or as its own package? Its kernels are pure and the heaviest code we have, andBigDecimalis compute-engine's class either way. -
The interface.
frontendandcomponentsalso import compute-engine, for compile targets, the Epsil printer,BoxedType, and engine construction. Does "only the nucleus" cover them (compile and Epsil subpaths), or is the interface allowed direct access? -
Tests. Is
createEnginefrom/testingthe rule for library tests too, or may a library's tests keep compute-engine as a dev dependency? -
Accessors.
engine'soperandsOfandsymbolNameOfpredate compute-engine's own guards. One set, which? -
Patches. Applied from a library's
declaretoday. In the nucleus, at engine construction for everyone, or declared by the library's records (requires: [patch])? The first changes behavior for libraries that don't ask for it. - Kernels. Decided (2026-10-04): the goal is for libraries, and the crux of a library is data and Epsil. Compiled Epsil won't match hand-written kernels soon, so a head carries both: its Epsil definition is the meaning and the source of truth, and an optional hand-written kernel is an accelerated implementation that must agree with it, checked by agreement tests and Plausible (the pattern BL-30's conversions already use, with the roles stated this way round). The host prefers the kernel when present; the record says which heads have one.
- Versioning. The nucleus's version follows its facade, and a compute-engine bump is patch-safe or library-visible. Who decides, the census run on the new nucleus?