Skip to content

Contributing

enumeratio edited this page Oct 13, 2026 · 10 revisions

Contributing

How to work on enumeratio: the toolchain, the names, where things go, and what a change has to pass before it lands. For parallel work across several sessions, see Lanes; for what to work on, see Roadmap.

Getting started

The repo is a pnpm monorepo on Vite+, whose CLI is vp. vp <name> runs a built-in command; vp run <name> runs a package.json script or a vite.config.ts task.

vp install
node tools/ci/dist-cache.ts build
vp check
vp test
  • Build the library packages before vp check or the tests: type-aware lint and the tests resolve sibling packages through their dist/. tools/ci/dist-cache.ts build [pkgs] builds them in dependency order and restores any package whose inputs haven't changed from a cache shared by every worktree, so a fresh worktree is ready in seconds. Within a package, the generated-definition steps skip themselves when their inputs are unchanged. BUILD_JOBS caps how many run at once.
  • Data derived from our own code (provenance, defined names, the crosswalk, ce-patches' patch list) is built, not committed, so a merge never leaves a file to regenerate. Only an external snapshot (a kernel's answers, a scrape, an expensive sweep) is committed, by its own collect-*/fetch-* script. A new src/patches/<slug>.ts in ce-patches registers itself.
  • Don't run a bare pnpm -r run build: it also builds the site, which takes more than 2 GB.
  • The site is VitePress in web/: pnpm --filter @enumeratio/web run build, or vitepress dev for the dev server.
  • If setup or package-manager behaviour looks wrong, vp env doctor says why.

Names

  • enumeratio is the mathematics: the symbol definitions (collections, statistics and maps, the special functions, the algebras) and their evaluation, built on @cortex-js/compute-engine and written in its language, Epsil.
  • notatio is the interface: the notebook, the <notatio-*> elements, the plots and glyphs, the CLI and REPL, the format registry, the docs site, and the vdom (the component form of an expression). notatio is not a syntax: the text is Epsil.
  • The line runs by what the word governs. enumeratio owns meaning ("a head", "defined as", "evaluates to"); notatio owns writing and showing ("written as", "prints as", InputForm, the *Form heads). See Syntax and Formats.
  • In the reference data, an example's retypeable text form is keyed epsil, and notatio keys its component serialisation, the vdom as Vue/React markup (Examples as Data §2).
  • Symbol names are spelled out, not abbreviated. Package names haven't all caught up; don't rename them in passing (see Component Naming for how renames wait).

Where a change goes

  • Design writing goes on this wiki, not in the repo: settled design under Design, open design under Speculative design.

  • User docs ship with their package: its README.md is the landing page, and docs/**/*.md are its further pages (order in front matter; a guide is one of these). The site serves them under /docs/<package>/; nothing is copied into web/. Link another package's docs by relative file path and the site absolutely (https://enumeratio.dev/…), so a page reads the same on GitHub and on the site.

  • Upstream candidates. A head or fix compute-engine would plausibly take (Wolfram or mpmath has it, or it fixes or widens a native head) is written as a patch in packages/ce-patches/, not in a symbol package, even before anything is proposed. Its code is laid out as in compute-engine; its reference entries stay in packages/reference/entries/ and never go upstream. See its README.

  • Reference entries. Each head's record is a folder, reference/<Head>/, in the package that declares it (packages/reference/entries/ for compute-engine's own heads):

    • index.md: front matter (summary, signatures, bindings, references…), with the details as a markdown list below it.
    • examples.tsv: one hand-written row per example, in page order. Each row has a stable id (lowercase words joined by -), expr and expected as MathJSON, and each system's hand classification. An example whose value is known from outside our evaluation also carries known, its source (DLMF 25.6.1, OEIS A000110…) and optionally a tolerance. Never "fix" a failing known check by rewriting expected.
    • examples.values.<system>.tsv: generated, one per kernel system: its in and its answers. Our own printed forms are built, not committed; examples.tsv pins a few as <form>.in/<form>.out columns, plus every <form>.back/.backOut. See Examples as Data.

    Read and write records through @enumeratio/entry/node, never by hand-parsing their files. After editing, run node packages/reference/scripts/format-records.ts. After adding or changing an example (or a printer or transpiler), run UPDATE_FORMS=1 node packages/frontend/scripts/collect-forms.ts; the forms test says when that's needed. After a merge conflict in examples.tsv, take one side, run node packages/frontend/scripts/pin-forms.ts, then collect-forms.ts. Don't name a variable e or i in an example: compute-engine lifts them to constants. Add a head by adding its folder.

Tests

  • Examples are the snapshots. A head's expected values are its reference examples (expected, known, and the generated examples.values.<system>.tsv), and a test of what a head evaluates to asserts against them, not a golden file of its own. A case too many to show on the page is a role: test example.
  • Golden JSON only for output that isn't a head's value (CLI demos, rendered forms, visuals): expect(actual).toEqual(golden[id]), regenerated behind an UPDATE_* flag. No toMatchSnapshot in packages/: a guard test forbids it.
  • Cross-check the routes. Where a fix can differ by route, the same inputs go through evaluate(), .N(), the parse route and the compiled code, and agree.
  • Patches in packages/ce-patches/ test the way their upstream does, snapshots included, so a test can go upstream with its code.
  • Long sweeps run nightly. A test that samples or enumerates takes a small budget by default and its full one under DEEP_TESTS=1. Keep the important cases in the standard run as fixed examples, not left to the sample. Don't just raise a test timeout.

Code

Much of this is what compute-engine's maintainer adds to the patches we send (see below); we hold our own code to it too.

  • Every route in the same change. A fix to what a head evaluates to also fixes .N(), simplification and each compile target (JavaScript, WGSL). A fact that must hold on several routes lives in one helper they all call, not a copy per route.

  • Numerics: decline rather than answer wrong. A kernel that can't vouch for its digits leaves the expression unevaluated: not NaN (a floating-point result with no value) and not a sentinel like 0. Before sending one, check:

    • convergence needs several consecutive small, decreasing terms and a tail bound, not one small term;
    • every loop has a cap (shifts, recurrences, retries), and past it the kernel declines;
    • error targets are relative to the result, and a result is returned only with the digits asked for;
    • guard radii and accuracy claims are measured on a grid against an oracle (mpmath), and stated as measured;
    • a float operand gives a float result;
    • the type handler never claims real on a branch cut or at a pole;
    • past the double range a value is carried scaled, or stays symbolic.
  • Comments state the current rule in the present tense, short and next to the code:

    • cite the formula (DLMF 25.11.1);
    • give the case that breaks without the code;
    • a guard threshold is a named constant, commented with the case that needs it.

    No history ("this used to…") and no bare references to a design section.

Upstreaming to compute-engine

What we ask compute-engine for is its issues (ours); what we carry until it ships is packages/ce-patches/ (its README has the patch lifecycle).

  • The issue and its PR together, the issue with a repro on compute-engine's main: the maintainer asks for a ready PR to come with the issue. Post the issue alone first only when the work may be rejected or needs his design call. Posted as enumeratio, and every draft is signed off in-house before it goes up. Batch related asks: compute-engine's maintainer prefers fewer issues.
  • The mathematics stays out of compute-engine's types. A pure-TS kernel that doesn't import compute-engine, with declare.ts the only file that does, is a port of one adapter file.
  • Use the Wolfram spelling when one exists. PowerModList replaced a bespoke SplitUnits head for exactly this reason, and the result was more flexible as well as more portable.
  • Never invent a head where widening an argument slot would do. Tier 2 is the model.
  • Every candidate ships with an independent oracle. Not a regression test — a second computation of the same number by different means: Dedekind sums against reciprocity, Burau against the closed-form torus polynomial, Ostrowski against Zeckendorf. That is what makes a contribution reviewable by someone who did not write it.

Writing a compute-engine PR

Before merging a PR we send, compute-engine's maintainer takes it as the base and pushes a review commit onto it. What those commits add is the checklist below; a PR that already does it lands as sent. Our own code follows the same rules ("Code", above).

  • Where code goes. Keep the files the fix touches; don't reorganise. A fact that must hold under evaluate(), .N() and simplify() lives in boxed-expression/, and all three import it.

  • Every lane in the same PR. The compiled JavaScript target, and the GPU targets where they apply, get the matching fix. Test that the compiled lane agrees with the interpreter.

  • Tests.

    • Jest describe/test, table-driven with a plain for over a typed cases array, and test names that carry the expected value.
    • Each case goes through evaluate(), .N(), .simplify(), the parse route and a compound argument that reduces to it.
    • A single-issue fix gets its own file, test/compute-engine/item-<issue>-<topic>.test.ts.
    • Explicit assertions, not snapshots, for anything with a semantic answer.
  • Numerics. Apply the checklist in "Code", above. Beyond it, the review passes also ask for:

    • a result that can't be represented stays symbolic;
    • the engine's caches (the Bernoulli table, say) are reused rather than recomputed;
    • the residual limits are recorded in compute-engine's ROADMAP.md.
  • Comments follow compute-engine's docs/COMMENTING-GUIDELINES.md: the current rule in the present tense, a formula reference (DLMF), the case that motivates the code, named guard constants. No history, no bare references to design sections.

  • CHANGELOG, in the same commit as the fix, under ## [Unreleased] → ### Issues Resolved:

    • a bold one-line claim about the resulting behaviour;
    • then concrete before-and-after values;
    • credit as (#N, contributed by [enumeratio](https://github.com/enumeratio)).

    After a rebase, check the entry is still under Unreleased: git slides it under a just-released version heading.

  • Commit messages. One long subject in clauses joined by ;, ending Fix #N (closes the issue) or Part of #N. The rationale goes in comments and the CHANGELOG, not the body.

  • CI. compute-engine's main is often red on its own: prettier drift in lint, corpus-pipeline, and single failing tests. Before fixing a red check on our PR, compare it with the same job on main; say nothing about checks that fail there too.

  • Allow edits by maintainers, so the review commit can land on our branch.

Git

  • Work on a branch or worktree, never directly on main. Land through a pull request: main requires its status checks, so a direct push skips them. Never force-push main.
  • Never commit conflict markers. After resolving any merge, and before committing: git grep -nE '^(<{7}|>{7})' $(git rev-parse --show-toplevel) must be empty. A marked file landing on main breaks the dev server and every checkout.
  • Commits and PRs are authored as the project (enumeratio), not as a person.
  • No attribution markers in commits, PR descriptions or comments: no Co-Authored-By or other trailers, no "generated with" line, no session links, no footers. This holds for any tool or agent that writes on the project's behalf. When merging a squash, give the message explicitly, since the default adds co-author trailers.

CI and deployment

  • The gate (ci.yml, every push and PR): build the library packages, then vp check, the per-package tests and the site build.
  • Production (enumeratio.dev) ships from Cloudflare Pages on merge to main; every build also gets a Cloudflare Pages preview at <sha7>.enumeratio.pages.dev.
  • Advisory sweeps (Plausible, the nightly and weekly oracle rescans) never block a merge; their failures file rolling nightly-fixup issues.

The workflows, secrets and artifacts are on CI and Deployment.

Home

Project

Design: every design page, settled, speculative and archived

Clone this wiki locally