npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

document-compute.js

v1.5.33

Published

Units-typed, tree-walking evaluator for document-schema.js's MathExpression -- exact-rational unit conversion, interval arithmetic, and bisection/Newton numeric solve-for, the compute package for the documents.js family.

Readme

document-compute.js

GitHub npm npm version CI

A units-typed, tree-walking evaluator for document-schema.js's MathExpressionevaluate() for point values and bounded intervals over the same interpreter, solveFor() for numeric root-finding (bisection and Newton's method) on one unknown. Exact-rational arithmetic for unit-conversion factors, so a chain of registry conversions never accumulates floating-point drift. The compute package for the documents.js family. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.

Created for ExaDev/documents.js#573: a document's formula is stored as a MathExpression tree (document-schema.js's src/math.ts, ExaDev/document-schema.js#15) — the semantic half of a ContentFormula, alongside the LaTeX a renderer serialises verbatim. Storing that tree buys nothing on its own; a document that states a formula and then reports its computed answer needs something that actually walks the tree and produces a number, unit-aware, without silently mixing dimensions that don't belong together. This package is that something: one interpreter, reused unchanged across three shapes of the same problem — a point value, a bounded interval, and (via root-finding) an unknown to solve for.

Getting started

Requires Node.js >=20 and pnpm 11.6.0.

pnpm install
pnpm build          # tsdown -> dist/ (ESM + CJS + .d.ts)
pnpm typecheck      # tsc -p tsconfig.json && tsc -p tsconfig.node.json (dual tsconfig)
pnpm lint           # eslint . --fix --cache --max-warnings 0
pnpm test           # vitest run
pnpm test:watch     # vitest
pnpm test:workers   # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate

To run a single test file, pass its path to vitest directly, e.g. pnpm exec vitest run src/compute/evaluate.test.ts.

What it provides

| Module | Exports | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | compute/rational | Rational, toRational, toExactRational, addRational, subtractRational, multiplyRational, divideRational, rationalToNumber | | compute/dimensions | dimensionExponent, dimensionsEqual, isDimensionless, multiplyDimensions, divideDimensions, scaleDimension, dimensionToString | | compute/quantity | quantity, addQuantities, subtractQuantities, multiplyQuantities, divideQuantities, negateQuantity, absQuantity, powQuantity, sqrtQuantity, sinQuantity, cosQuantity, tanQuantity | | compute/interval | interval, pointInterval, addIntervals, subtractIntervals, multiplyIntervals, divideIntervals, negateInterval, absInterval | | compute/evaluate | evaluate, EvaluationResult, isInterval | | compute/solve | solveFor, SolveMethod, SolveForOptions | | compute/errors | IncompatibleDimensionsError, UnboundSymbolError, UnknownUnitError, DivisionByZeroError, UnsupportedExpressionError, NumericDomainError, NonConvergentSolveError |

Every module in the table is re-exported from the package root, so its exports import from 'document-compute.js' directly.

The value types this evaluator consumes and produces — Quantity, Interval, EvaluationValue, FormulaBindings, and their Zod schemas — are not defined here: they are typed contracts in document-schema.js itself (src/math.ts, beside MathExpression), so evaluation inputs are schema-validated shapes like everything else in that package. Import them from 'document-schema.js' the same way this package does:

import { evaluate } from "document-compute.js";
import type { FormulaBindings, MathExpression } from "document-schema.js";

// F = m * a
const force: MathExpression = {
  kind: "app",
  operator: "math:multiply",
  args: [
    { kind: "sym", id: "m" },
    { kind: "sym", id: "a" },
  ],
};

const bindings: FormulaBindings = {
  m: { kind: "quantity", magnitude: 2, dimension: { mass: 1 } },
  a: { kind: "quantity", magnitude: 3, dimension: { length: 1, time: -2 } },
};

const result = evaluate(force, bindings);
// { kind: 'quantity', magnitude: 6, dimension: { mass: 1, length: 1, time: -2 } }

evaluate never returns a failure inside its result: a successful call returns a plain Quantity | Interval (see below on why the return type is not literally Quantity | Interval | error), and every real failure throws one of compute/errors' own classes — the same idiom document-schema.js's schema-io.ts and archive-codec's CompoundFileFormatError/ArchiveWalkLimitError already use: a named, catchable Error subclass, never a { ok, error } wrapper folded into the return type.

Units as the type system

Every Quantity this evaluator produces carries a dimension — an SI exponent vector reusing document-schema.js's own DimensionVector ({ length: 1, time: -1 } for speed, {} for dimensionless) — alongside a plain magnitude. Adding or subtracting two quantities whose dimensions don't match is not a number this package will produce: addQuantities/subtractQuantities (and the identical rule inside evaluate for math:add/math:subtract) throw IncompatibleDimensionsError rather than returning a value that happens to be wrong. Multiplication and division are always dimensionally defined — they combine dimension vectors by adding or subtracting exponents (compute/dimensions.ts's multiplyDimensions/divideDimensions) — so there is nothing to reject there, only a resulting dimension to compute. powQuantity/sqrtQuantity extend the same rule to exponents: a dimensionless base tolerates any real exponent, a dimensioned one only an integer power whose scaled exponents stay integers (DimensionVectorSchema requires integers, so sqrt of length^1 has no answer and throws — IncompatibleDimensionsError again, not a fractional dimension nobody asked for).

A MathExpression's 'qty' leaf carries an exact value plus a unit-registry id (document-schema.js's MathQty/MathUnit, resolved against the SymbolTable passed as evaluate's third argument); evaluateQty (compute/evaluate.ts) resolves that id, then computes si_value = value * factorToSi + offsetToSientirely in exact BigInt rational arithmetic (compute/rational.ts), converting to a plain JS number exactly once, at the moment the resolved SI-coherent magnitude enters the evaluator as a Quantity. That is the one deliberate exactness boundary in this package: a chain of unit-registry conversions (feet to metres, an affine temperature scale, a per-unit-normalised power-system quantity) never compounds floating-point rounding the way repeated Number multiplication would, because every step upstream of that single conversion is bit-exact BigInt arithmetic, reduced to lowest terms at every operation. Downstream of that boundary — ordinary +/-/*// between already-resolved Quantity magnitudes, sin/cos/sqrt, a solveFor root — is plain floating point, because those results are not exact in general (there is no exact rational sin(1)), and holding them to bit-exactness would be false precision, not a stronger guarantee. QuantitySchema's own field comment on magnitude states this trade-off; it is a judgement call this package makes deliberately, not an oversight.

Interval arithmetic, over the same evaluator

The issue's own example is a compliance region: 0.87 <= cos(phi) <= 1. Rather than adding a second evaluator for "a formula, but with ranges," FormulaBindings lets any symbol be bound to an Interval ({ kind: 'interval', min, max, dimension }) instead of a point Quantity, and evaluate's 'app' dispatch promotes a plain Quantity operand to a degenerate point interval (pointInterval) the moment either side of a binary operator is an Interval — the same tree walk, the same operator ids, just running over ranges instead of points once it notices one. addIntervals/subtractIntervals combine endpoints directly; multiplyIntervals/divideIntervals implement the standard rule that a product's or quotient's extremes are always attained at one of the four corner combinations of the two intervals' endpoints (min*min, min*max, max*min, max*max, or the equivalent via the reciprocal for division) — which is what actually resolves the textbook sign-case table (positive×positive, negative×negative, straddling×straddling, and every mixed case) into one formula that is correct regardless of which side of zero either interval sits on; interval.test.ts exercises each sign combination directly rather than trusting the closed form on faith. Division by an interval that touches or straddles zero has no defined result (it would pass through ±Infinity) and throws DivisionByZeroError instead of letting Infinity/NaN flow silently into the rest of a computation. Only the four arithmetic operators plus negate/abs have interval rules in this pass — pow/sqrt/the trig functions are Quantity-only and throw UnsupportedExpressionError on an Interval operand, since a correct general interval range for a non-monotonic or sign-dependent function needs more analysis than this pass's scope covers (see below).

Numeric solve-for

solveFor(expression, targetValue, unknownSymbol, bindings, options?, context?) finds the magnitude for unknownSymbol that makes expression evaluate to targetValue, by root-finding over the same evaluate — never by rearranging the expression algebraically. It implements both algorithms the issue asks for and lets options.method pick between them ('bisection', the default, or 'newton'):

  • Bisection needs options.bracket: [low, high] whose residuals have opposite signs (the intermediate-value theorem is its entire correctness argument), halves the bracket every iteration, and cannot diverge — the safe default.
  • Newton's method needs options.initialGuess and estimates the derivative by central difference, (f(x+h) - f(x-h)) / (2h) (options.derivativeStep, default 1e-6) — chosen over a one-sided forward/backward difference because its truncation error is O(h²) rather than O(h). No symbolic derivative is available without a symbolic layer this pass deliberately does not build (see Out of scope), so a numeric one is the whole story here.

Both throw NonConvergentSolveError rather than returning a number they cannot vouch for: bisection when its bracket doesn't straddle a root or the iteration budget (options.maxIterations, default 100) runs out before the residual drops under options.tolerance (default 1e-9); Newton when the numeric derivative vanishes or diverges, or the same budget/tolerance is exhausted. options.unknownDimension sets the DimensionVector the unknown is bound under at each trial point (default dimensionless) so a physically dimensioned unknown (a length, a mass) solves correctly against a formula that checks dimensions along the way.

import { solveFor } from "document-compute.js";
import type { MathExpression } from "document-schema.js";

// x^2 = 4, solve for x
const xSquared: MathExpression = {
  kind: "app",
  operator: "math:pow",
  args: [
    { kind: "sym", id: "x" },
    { kind: "num", numerator: "2", denominator: "1" },
  ],
};

solveFor(xSquared, 4, "x", {}, { bracket: [0, 3] }); // 2, via bisection
solveFor(xSquared, 4, "x", {}, { method: "newton", initialGuess: 3 }); // 2, via Newton

Deviations from the issue

One thing #573 asks for was closed at adoption rather than built here: Quantity and FormulaBindings (with Interval and EvaluationValue) are typed contracts in document-schema.js's src/math.ts, beside MathExpression itself, exactly as the issue proposes — evaluation inputs are validated schemas like everything else in that package, and this package imports them from there the same way it imports MathExpression, DimensionVector, and ExactRational, rather than carrying package-local definitions.

The worked-example differential harness

ExaDev/documents.js#794 split #573's own stated differentiator — measuring the fraction of a real document's formulae whose evaluation reproduces the document's own stated answer — into its own package once evaluate/solveFor themselves existed to measure against. src/harness/worked-example.ts and src/harness/corpus.ts are that harness:

  • runWorkedExampleSequence(formulas, symbolTable?, options?) walks a document-ordered sequence of already-lowered ContentFormula values and recognises the "givens, a formula, a stated result" shape a worked example actually has: a definition (F = m \times a — the right-hand side still mentions a symbol), a binding (m = 2 kg — a fully closed "given"), and a stated result (F = 6 N — structurally identical to a binding, but restating a symbol a definition is waiting on). A definition's own right-hand side is evaluated against whatever bindings are current when its stated result is reached, not a snapshot taken when the definition line first appeared — the common real document states the general law first, then the specific numbers, then the answer. Every outcome is one of match, mismatch, gap (naming a specific WorkedExampleGapunbound-symbol, unknown-unit, incompatible-dimensions, division-by-zero, unsupported-construct, numeric-domain, non-convergent-solve — one per compute/errors.ts class), or unresolved (a definition the document never restated an answer for). Comparison is by relative tolerance (1e-3 default), not exact equality, since a worked example's own stated answer is conventionally rounded.
  • collectFormulas(document)/runCorpus(documents, options?) extract the formula sequence out of a wordprocessing ContentDocument's block flow (table cells included) and run the harness over a whole corpus at once, aggregating one combined coverage fraction plus every document's own outcomes; formatCorpusReport renders the result as plain text for a CLI/console caller.

Scoped to point-valued (Quantity) answers: every value this harness computes comes from evaluating a closed statement with no bindings, which evaluate cannot turn into an Interval (an Interval only ever arises by binding a symbol to one) — a genuinely interval-valued worked example (0.87 <= cos(phi) <= 1, #573's own illustration of interval arithmetic) has no representation in this "symbol = expression" equality grammar at all, since neither MathExpression nor documents.js's LaTeX lowering has a compound-inequality-to-range reading, and is out of scope for this pass rather than silently mishandled.

src/harness/corpus.test.ts proves the whole pipeline end to end — markdown text through markdown-codec's $$ block recognition and documents.js's lowerMarkdownMath (the "LaTeX lowering" #794 names as the natural source of worked examples) into this harness — against a small, hand-authored starter corpus. markdown-codec and documents.js are devDependencies only: both sit above this package in the family's own dependency order (see the monorepo root README's package table), so neither can be a runtime dependency here without a cycle. The harness itself is not test-only, though — src/index.ts exports it as real public API (runWorkedExampleSequence, collectFormulas, runCorpus, formatCorpusReport), the same as evaluate/solveFor; what's actually true is narrower: it has no runtime consumer anywhere else in the family yet, unlike evaluate/solveFor below it, which document-mcp's compute_formula tool does depend on directly (see Conventions). An at-scale corpus layer now exists: pnpm test:corpus runs the gitignored test/corpus/ suite that scripts/generate-corpus.mjs regenerates — 300 deterministic worked-example documents over the mechanically-lowered arithmetic grammar (explicit \times/\frac/\sqrt compositions, never juxtaposition), each stated answer computed by the generator's own parallel evaluation — measuring 300/300 matched, 0 gaps, 0 unresolved, with the measured report written to test/corpus/report.txt. Its first run paid for itself immediately: every document with a NEGATIVE stated answer degraded its whole equality under the lowering's leading-minus-only unary reading, which now generalises past that (a minus after a relation or operator signs the following operand). Real textbook corpora remain a local addition on top: point the same test/corpus/files/ directory at real markdown documents and the harness measures them identically.

While building this harness's own fixtures, a real bug surfaced in documents.js's LaTeX lowering: F = m \times a (the textbook-standard way to write almost any formula) lowers to (F = m) \times a rather than F = (m \times a), because the lowering folds relational and arithmetic operators at the same precedence with no notion that = should bind loosest — filed as ExaDev/documents.js#812. This package's own fixtures work around it with an explicit braced right-hand side (F = {m \times a}, which lowers correctly), since fixing the lowering itself is out of scope for this package.

Out of scope

Quoting the issue's own scope line directly: this is deliberately not a CAS in the Mathematica sense — units-typed evaluation and numeric solving are the 90% of "compute the result of a formula from a document" and are buildable natively now. Concretely, this pass does not attempt, and this package carries no code toward:

  • Symbolic algebra — exact rearrangement of an expression emitted back out as LaTeX, simplification, integration. solveFor finds a root numerically; it never isolates the unknown algebraically.
  • A SymPy sidecar or any other symbolic-engine adapter. The issue names this as the eventual home for symbolic work, behind an evaluator interface this package does not define or stub.
  • Matrix-valued evaluation. MathExpression's 'matrix' node exists in the grammar document-schema.js defines, but this evaluator only ever produces scalar Quantity/Interval values; a 'matrix' node throws UnsupportedExpressionError rather than being silently misevaluated.
  • A general interval rule for pow/sqrt/the trigonometric operators. These are implemented for Quantity only; applied to an Interval operand they throw UnsupportedExpressionError rather than guessing at a range a non-monotonic or sign-dependent function would need real analysis to get right.

Conventions

  • Worker-isomorphic (see the family-wide convention): runtime src/ must not import node:*, a bare Node builtin, or use the Buffer global — enforced by a no-restricted-imports/no-restricted-globals ESLint rule and exercised in CI by running a test suite inside an actual workerd isolate (pnpm test:workers). Exact-rational arithmetic is plain BigInt, never node:crypto or any other Node-only primitive, precisely so this holds.
  • Only src/index.ts may be named index.* — a custom ESLint rule (local/no-non-barrel-index) rejects any other module using an index basename, since that would be a hidden entry point the exports map in package.json doesn't advertise.
  • Failure is always a thrown, named Error subclass (compute/errors.ts), never a { ok, error } result wrapper — matching document-schema.js's schema-io.ts and archive-codec's own error classes rather than inventing a second convention for this package alone.
  • Wired into document-mcp since ExaDev/documents.js#928: its compute_formula tool is a real runtime dependent, reading a document's embedded formulas and evaluating each through evaluate(). Still not a dependency of documents.js, document-cli, or web — this package sits above document-schema.js alone in the family's own dependency order, and document-mcp was the first surface with a natural, agent-facing need for a document's formula actually computed; wiring it into any further surface remains a separate, deliberate decision for that surface.
  • Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the monorepo root README for the mechanism (topological per-package semantic-release via @exadev/semantic-release-workspace, OIDC trusted npm publishing, and the post-release republish/attestation jobs).

Install

pnpm add document-compute.js
# or
npm install document-compute.js

License

MIT