document-compute.js 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -26
- package/dist/index.cjs +234 -15
- package/dist/index.d.cts +67 -5
- package/dist/index.d.ts +67 -5
- package/dist/index.js +231 -16
- package/package.json +4 -11
package/README.md
CHANGED
|
@@ -24,34 +24,37 @@ To run a single test file, pass its path to vitest directly, e.g. `pnpm exec vit
|
|
|
24
24
|
|
|
25
25
|
## What it provides
|
|
26
26
|
|
|
27
|
-
| Module
|
|
28
|
-
|
|
29
|
-
| `compute/rational`
|
|
30
|
-
| `compute/dimensions` | `dimensionExponent`, `dimensionsEqual`, `isDimensionless`, `multiplyDimensions`, `divideDimensions`, `scaleDimension`, `dimensionToString`
|
|
31
|
-
| `compute/quantity`
|
|
32
|
-
| `compute/interval`
|
|
33
|
-
| `compute/evaluate`
|
|
34
|
-
| `compute/solve`
|
|
35
|
-
| `compute/errors`
|
|
27
|
+
| Module | Exports |
|
|
28
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `compute/rational` | `Rational`, `toRational`, `toExactRational`, `addRational`, `subtractRational`, `multiplyRational`, `divideRational`, `rationalToNumber` |
|
|
30
|
+
| `compute/dimensions` | `dimensionExponent`, `dimensionsEqual`, `isDimensionless`, `multiplyDimensions`, `divideDimensions`, `scaleDimension`, `dimensionToString` |
|
|
31
|
+
| `compute/quantity` | `quantity`, `addQuantities`, `subtractQuantities`, `multiplyQuantities`, `divideQuantities`, `negateQuantity`, `absQuantity`, `powQuantity`, `sqrtQuantity`, `sinQuantity`, `cosQuantity`, `tanQuantity` |
|
|
32
|
+
| `compute/interval` | `interval`, `pointInterval`, `addIntervals`, `subtractIntervals`, `multiplyIntervals`, `divideIntervals`, `negateInterval`, `absInterval` |
|
|
33
|
+
| `compute/evaluate` | `evaluate`, `EvaluationResult`, `isInterval` |
|
|
34
|
+
| `compute/solve` | `solveFor`, `SolveMethod`, `SolveForOptions` |
|
|
35
|
+
| `compute/errors` | `IncompatibleDimensionsError`, `UnboundSymbolError`, `UnknownUnitError`, `DivisionByZeroError`, `UnsupportedExpressionError`, `NumericDomainError`, `NonConvergentSolveError` |
|
|
36
36
|
|
|
37
37
|
Every module in the table is re-exported from the package root, so its exports import from `'document-compute.js'` directly.
|
|
38
38
|
|
|
39
39
|
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:
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
import { evaluate } from
|
|
43
|
-
import type { FormulaBindings, MathExpression } from
|
|
42
|
+
import { evaluate } from "document-compute.js";
|
|
43
|
+
import type { FormulaBindings, MathExpression } from "document-schema.js";
|
|
44
44
|
|
|
45
45
|
// F = m * a
|
|
46
46
|
const force: MathExpression = {
|
|
47
|
-
kind:
|
|
48
|
-
operator:
|
|
49
|
-
args: [
|
|
47
|
+
kind: "app",
|
|
48
|
+
operator: "math:multiply",
|
|
49
|
+
args: [
|
|
50
|
+
{ kind: "sym", id: "m" },
|
|
51
|
+
{ kind: "sym", id: "a" },
|
|
52
|
+
],
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
const bindings: FormulaBindings = {
|
|
53
|
-
m: { kind:
|
|
54
|
-
a: { kind:
|
|
56
|
+
m: { kind: "quantity", magnitude: 2, dimension: { mass: 1 } },
|
|
57
|
+
a: { kind: "quantity", magnitude: 3, dimension: { length: 1, time: -2 } },
|
|
55
58
|
};
|
|
56
59
|
|
|
57
60
|
const result = evaluate(force, bindings);
|
|
@@ -80,25 +83,39 @@ The issue's own example is a compliance region: `0.87 <= cos(phi) <= 1`. Rather
|
|
|
80
83
|
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.
|
|
81
84
|
|
|
82
85
|
```ts
|
|
83
|
-
import { solveFor } from
|
|
84
|
-
import type { MathExpression } from
|
|
86
|
+
import { solveFor } from "document-compute.js";
|
|
87
|
+
import type { MathExpression } from "document-schema.js";
|
|
85
88
|
|
|
86
89
|
// x^2 = 4, solve for x
|
|
87
90
|
const xSquared: MathExpression = {
|
|
88
|
-
kind:
|
|
89
|
-
operator:
|
|
90
|
-
args: [
|
|
91
|
+
kind: "app",
|
|
92
|
+
operator: "math:pow",
|
|
93
|
+
args: [
|
|
94
|
+
{ kind: "sym", id: "x" },
|
|
95
|
+
{ kind: "num", numerator: "2", denominator: "1" },
|
|
96
|
+
],
|
|
91
97
|
};
|
|
92
98
|
|
|
93
|
-
solveFor(xSquared, 4,
|
|
94
|
-
solveFor(xSquared, 4,
|
|
99
|
+
solveFor(xSquared, 4, "x", {}, { bracket: [0, 3] }); // 2, via bisection
|
|
100
|
+
solveFor(xSquared, 4, "x", {}, { method: "newton", initialGuess: 3 }); // 2, via Newton
|
|
95
101
|
```
|
|
96
102
|
|
|
97
103
|
## Deviations from the issue
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
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.
|
|
100
106
|
|
|
101
|
-
The
|
|
107
|
+
## The worked-example differential harness
|
|
108
|
+
|
|
109
|
+
[ExaDev/documents.js#794](https://github.com/ExaDev/documents.js/issues/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:
|
|
110
|
+
|
|
111
|
+
- `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 `WorkedExampleGap` — `unbound-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.
|
|
112
|
+
- `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.
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
`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, which is exactly why this package remains "not wired into the conversion pipeline" at runtime even though its own test suite now exercises that pipeline. A large real-world corpus (the issue's own stated differentiator at scale) is not included — gathering one is a data-curation task, not a code one — but is a straightforward local addition: point a `test/corpus/` directory (gitignored, matching `pdf-codec`'s own `test:corpus` convention) at real markdown documents with worked examples and feed `readMarkdownContent` → `lowerMarkdownMath` → `runCorpus` the same way `corpus.test.ts` does.
|
|
117
|
+
|
|
118
|
+
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](https://github.com/ExaDev/documents.js/issues/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.
|
|
102
119
|
|
|
103
120
|
## Out of scope
|
|
104
121
|
|
|
@@ -106,7 +123,6 @@ Quoting the issue's own scope line directly: **this is deliberately not a CAS in
|
|
|
106
123
|
|
|
107
124
|
- **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.
|
|
108
125
|
- **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.
|
|
109
|
-
- **The worked-example test harness** — see Deviations from the issue above.
|
|
110
126
|
- **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.
|
|
111
127
|
- **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.
|
|
112
128
|
|
package/dist/index.cjs
CHANGED
|
@@ -277,7 +277,7 @@ function absInterval(a) {
|
|
|
277
277
|
}
|
|
278
278
|
//#endregion
|
|
279
279
|
//#region src/compute/evaluate.ts
|
|
280
|
-
const EMPTY_SYMBOL_TABLE$
|
|
280
|
+
const EMPTY_SYMBOL_TABLE$2 = {
|
|
281
281
|
symbols: [],
|
|
282
282
|
units: []
|
|
283
283
|
};
|
|
@@ -287,7 +287,7 @@ function isInterval(value) {
|
|
|
287
287
|
function toInterval(value) {
|
|
288
288
|
return isInterval(value) ? value : pointInterval(value.magnitude, value.dimension);
|
|
289
289
|
}
|
|
290
|
-
function asQuantity(value, context) {
|
|
290
|
+
function asQuantity$1(value, context) {
|
|
291
291
|
if (isInterval(value)) throw new UnsupportedExpressionError(context, "this position requires a plain Quantity, not an Interval");
|
|
292
292
|
return value;
|
|
293
293
|
}
|
|
@@ -323,7 +323,7 @@ const UNARY_OPERATORS = {
|
|
|
323
323
|
"math:cos": { quantity: cosQuantity },
|
|
324
324
|
"math:tan": { quantity: tanQuantity }
|
|
325
325
|
};
|
|
326
|
-
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$
|
|
326
|
+
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$2) {
|
|
327
327
|
switch (expression.kind) {
|
|
328
328
|
case "num": return quantity(rationalToNumber(toRational(expression)), {});
|
|
329
329
|
case "qty": return evaluateQty(expression, context);
|
|
@@ -346,19 +346,27 @@ function evaluateQty(node, context) {
|
|
|
346
346
|
if (unit.offsetToSi !== void 0) siValue = addRational(siValue, toRational(unit.offsetToSi));
|
|
347
347
|
return quantity(rationalToNumber(siValue), unit.dimension);
|
|
348
348
|
}
|
|
349
|
+
function expectTwoArgs(args, subject) {
|
|
350
|
+
const [left, right] = args;
|
|
351
|
+
if (args.length !== 2 || left === void 0 || right === void 0) throw new UnsupportedExpressionError("evaluate", `${subject} takes exactly 2 arguments, got ${args.length}`);
|
|
352
|
+
return [left, right];
|
|
353
|
+
}
|
|
354
|
+
function expectOneArg(args, subject) {
|
|
355
|
+
const [only] = args;
|
|
356
|
+
if (args.length !== 1 || only === void 0) throw new UnsupportedExpressionError("evaluate", `${subject} takes exactly 1 argument, got ${args.length}`);
|
|
357
|
+
return only;
|
|
358
|
+
}
|
|
349
359
|
function evaluateApp(node, bindings, context) {
|
|
350
360
|
const args = node.args.map((arg) => evaluate(arg, bindings, context));
|
|
351
361
|
const binary = BINARY_OPERATORS[node.operator];
|
|
352
362
|
if (binary !== void 0) {
|
|
353
|
-
|
|
354
|
-
const [left, right] = args;
|
|
363
|
+
const [left, right] = expectTwoArgs(args, `operator '${node.operator}'`);
|
|
355
364
|
if (isInterval(left) || isInterval(right)) return binary.interval(toInterval(left), toInterval(right));
|
|
356
365
|
return binary.quantity(left, right);
|
|
357
366
|
}
|
|
358
367
|
const unary = UNARY_OPERATORS[node.operator];
|
|
359
368
|
if (unary !== void 0) {
|
|
360
|
-
|
|
361
|
-
const [only] = args;
|
|
369
|
+
const only = expectOneArg(args, `operator '${node.operator}'`);
|
|
362
370
|
if (isInterval(only)) {
|
|
363
371
|
if (unary.interval === void 0) throw new UnsupportedExpressionError("evaluate", `operator '${node.operator}' has no interval rule in this pass`);
|
|
364
372
|
return unary.interval(only);
|
|
@@ -366,15 +374,14 @@ function evaluateApp(node, bindings, context) {
|
|
|
366
374
|
return unary.quantity(only);
|
|
367
375
|
}
|
|
368
376
|
if (node.operator === "math:pow") {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
return powQuantity(asQuantity(base, "evaluate"), asQuantity(exponent, "evaluate"));
|
|
377
|
+
const [base, exponent] = expectTwoArgs(args, "'math:pow'");
|
|
378
|
+
return powQuantity(asQuantity$1(base, "evaluate"), asQuantity$1(exponent, "evaluate"));
|
|
372
379
|
}
|
|
373
380
|
throw new UnsupportedExpressionError("evaluate", `unknown operator '${node.operator}'`);
|
|
374
381
|
}
|
|
375
382
|
function evaluateBinder(node, bindings, context) {
|
|
376
|
-
const lower = asQuantity(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
377
|
-
const upper = asQuantity(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
383
|
+
const lower = asQuantity$1(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
384
|
+
const upper = asQuantity$1(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
378
385
|
if (!isDimensionless(lower.dimension) || !isDimensionless(upper.dimension)) throw new IncompatibleDimensionsError(`math:${node.kind}`, lower.dimension, upper.dimension, "binder bounds must be dimensionless");
|
|
379
386
|
if (!Number.isInteger(lower.magnitude) || !Number.isInteger(upper.magnitude)) throw new UnsupportedExpressionError(`evaluate:${node.kind}`, "binder bounds must evaluate to integers");
|
|
380
387
|
let accumulator = node.kind === "sum" ? quantity(0, {}) : quantity(1, {});
|
|
@@ -383,7 +390,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
383
390
|
...bindings,
|
|
384
391
|
[node.binder]: quantity(i, {})
|
|
385
392
|
};
|
|
386
|
-
const bodyValue = asQuantity(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
393
|
+
const bodyValue = asQuantity$1(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
387
394
|
accumulator = node.kind === "sum" ? addQuantities(accumulator, bodyValue) : multiplyQuantities(accumulator, bodyValue);
|
|
388
395
|
}
|
|
389
396
|
return accumulator;
|
|
@@ -393,7 +400,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
393
400
|
const DEFAULT_TOLERANCE = 1e-9;
|
|
394
401
|
const DEFAULT_MAX_ITERATIONS = 100;
|
|
395
402
|
const DEFAULT_DERIVATIVE_STEP = 1e-6;
|
|
396
|
-
const EMPTY_SYMBOL_TABLE = {
|
|
403
|
+
const EMPTY_SYMBOL_TABLE$1 = {
|
|
397
404
|
symbols: [],
|
|
398
405
|
units: []
|
|
399
406
|
};
|
|
@@ -407,7 +414,7 @@ function residualFn(expression, targetValue, unknownSymbol, bindings, context, d
|
|
|
407
414
|
return result.magnitude - targetValue;
|
|
408
415
|
};
|
|
409
416
|
}
|
|
410
|
-
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE) {
|
|
417
|
+
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE$1) {
|
|
411
418
|
const method = options.method ?? "bisection";
|
|
412
419
|
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
|
|
413
420
|
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
@@ -449,6 +456,214 @@ function newton(f, initialGuess, tolerance, maxIterations, h) {
|
|
|
449
456
|
throw new NonConvergentSolveError("newton", maxIterations, `residual still exceeds tolerance ${tolerance} after ${maxIterations} iterations`);
|
|
450
457
|
}
|
|
451
458
|
//#endregion
|
|
459
|
+
//#region src/harness/worked-example.ts
|
|
460
|
+
const DEFAULT_RELATIVE_TOLERANCE = .001;
|
|
461
|
+
const EMPTY_BINDINGS = {};
|
|
462
|
+
const EMPTY_SYMBOL_TABLE = {
|
|
463
|
+
symbols: [],
|
|
464
|
+
units: []
|
|
465
|
+
};
|
|
466
|
+
function asEquality(expression) {
|
|
467
|
+
if (expression.kind !== "app" || expression.operator !== "math:eq") return;
|
|
468
|
+
const [lhs, rhs] = expression.args;
|
|
469
|
+
if (lhs === void 0 || rhs === void 0 || lhs.kind !== "sym") return;
|
|
470
|
+
return {
|
|
471
|
+
targetSymbol: lhs.id,
|
|
472
|
+
rhs
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function containsSymbol(expression) {
|
|
476
|
+
switch (expression.kind) {
|
|
477
|
+
case "sym": return true;
|
|
478
|
+
case "num":
|
|
479
|
+
case "qty":
|
|
480
|
+
case "unparsed": return false;
|
|
481
|
+
case "app": return expression.args.some(containsSymbol);
|
|
482
|
+
case "sum":
|
|
483
|
+
case "prod": return containsSymbol(expression.lower) || containsSymbol(expression.upper) || containsSymbol(expression.body);
|
|
484
|
+
case "matrix": return expression.rows.some((row) => row.some(containsSymbol));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function asQuantity(value) {
|
|
488
|
+
if (isInterval(value)) throw new UnsupportedExpressionError("runWorkedExampleSequence", "this harness compares point-valued Quantity answers only; a symbol resolving to a range (Interval) has no stated-answer comparison defined yet");
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
function gapFromError(error) {
|
|
492
|
+
if (error instanceof UnboundSymbolError) return "unbound-symbol";
|
|
493
|
+
if (error instanceof UnknownUnitError) return "unknown-unit";
|
|
494
|
+
if (error instanceof IncompatibleDimensionsError) return "incompatible-dimensions";
|
|
495
|
+
if (error instanceof DivisionByZeroError) return "division-by-zero";
|
|
496
|
+
if (error instanceof UnsupportedExpressionError) return "unsupported-construct";
|
|
497
|
+
if (error instanceof NumericDomainError) return "numeric-domain";
|
|
498
|
+
if (error instanceof NonConvergentSolveError) return "non-convergent-solve";
|
|
499
|
+
return "other-evaluation-error";
|
|
500
|
+
}
|
|
501
|
+
function errorMessage(error) {
|
|
502
|
+
return error instanceof Error ? error.message : String(error);
|
|
503
|
+
}
|
|
504
|
+
function withinTolerance(actual, expected, relativeTolerance) {
|
|
505
|
+
if (expected === 0) return Math.abs(actual) <= relativeTolerance;
|
|
506
|
+
return Math.abs(actual - expected) / Math.abs(expected) <= relativeTolerance;
|
|
507
|
+
}
|
|
508
|
+
function resultsMatch(actual, expected, relativeTolerance) {
|
|
509
|
+
return dimensionsEqual(actual.dimension, expected.dimension) && withinTolerance(actual.magnitude, expected.magnitude, relativeTolerance);
|
|
510
|
+
}
|
|
511
|
+
function runWorkedExampleSequence(formulas, symbolTable = EMPTY_SYMBOL_TABLE, options) {
|
|
512
|
+
const relativeTolerance = options?.relativeTolerance ?? DEFAULT_RELATIVE_TOLERANCE;
|
|
513
|
+
const bindings = {};
|
|
514
|
+
const outcomes = [];
|
|
515
|
+
let pending;
|
|
516
|
+
const closeUnresolved = () => {
|
|
517
|
+
if (pending === void 0) return;
|
|
518
|
+
outcomes.push({
|
|
519
|
+
outcome: "unresolved",
|
|
520
|
+
targetSymbol: pending.targetSymbol,
|
|
521
|
+
message: `"${pending.targetSymbol}" was defined but the sequence never restated it as a closed numeric result before ending or being superseded by another definition`
|
|
522
|
+
});
|
|
523
|
+
pending = void 0;
|
|
524
|
+
};
|
|
525
|
+
for (const formula of formulas) {
|
|
526
|
+
if (formula.content === void 0) continue;
|
|
527
|
+
const equality = asEquality(formula.content);
|
|
528
|
+
if (equality === void 0) continue;
|
|
529
|
+
const { targetSymbol, rhs } = equality;
|
|
530
|
+
if (containsSymbol(rhs)) {
|
|
531
|
+
closeUnresolved();
|
|
532
|
+
pending = {
|
|
533
|
+
targetSymbol,
|
|
534
|
+
rhs
|
|
535
|
+
};
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
let closedValue;
|
|
539
|
+
try {
|
|
540
|
+
closedValue = asQuantity(evaluate(rhs, EMPTY_BINDINGS, symbolTable));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
outcomes.push({
|
|
543
|
+
outcome: "gap",
|
|
544
|
+
gap: gapFromError(error),
|
|
545
|
+
targetSymbol,
|
|
546
|
+
message: errorMessage(error)
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (pending?.targetSymbol === targetSymbol) {
|
|
551
|
+
const { rhs: definitionRhs } = pending;
|
|
552
|
+
pending = void 0;
|
|
553
|
+
let actual;
|
|
554
|
+
try {
|
|
555
|
+
actual = asQuantity(evaluate(definitionRhs, bindings, symbolTable));
|
|
556
|
+
} catch (error) {
|
|
557
|
+
outcomes.push({
|
|
558
|
+
outcome: "gap",
|
|
559
|
+
gap: gapFromError(error),
|
|
560
|
+
targetSymbol,
|
|
561
|
+
message: errorMessage(error)
|
|
562
|
+
});
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
outcomes.push(resultsMatch(actual, closedValue, relativeTolerance) ? {
|
|
566
|
+
outcome: "match",
|
|
567
|
+
targetSymbol,
|
|
568
|
+
expected: closedValue,
|
|
569
|
+
actual
|
|
570
|
+
} : {
|
|
571
|
+
outcome: "mismatch",
|
|
572
|
+
targetSymbol,
|
|
573
|
+
expected: closedValue,
|
|
574
|
+
actual
|
|
575
|
+
});
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
bindings[targetSymbol] = closedValue;
|
|
579
|
+
}
|
|
580
|
+
closeUnresolved();
|
|
581
|
+
const matched = outcomes.filter((o) => o.outcome === "match").length;
|
|
582
|
+
const mismatched = outcomes.filter((o) => o.outcome === "mismatch").length;
|
|
583
|
+
const gaps = outcomes.filter((o) => o.outcome === "gap").length;
|
|
584
|
+
const unresolved = outcomes.filter((o) => o.outcome === "unresolved").length;
|
|
585
|
+
return {
|
|
586
|
+
outcomes,
|
|
587
|
+
total: outcomes.length,
|
|
588
|
+
matched,
|
|
589
|
+
mismatched,
|
|
590
|
+
gaps,
|
|
591
|
+
unresolved,
|
|
592
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/harness/corpus.ts
|
|
597
|
+
function collectFormulasFromBlocks(blocks, out) {
|
|
598
|
+
for (const block of blocks) {
|
|
599
|
+
if (block.kind === "table") {
|
|
600
|
+
for (const row of block.rows) for (const cell of row.cells) collectFormulasFromBlocks(cell.blocks, out);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (block.kind === "embeddedObject" && block.objectKind === "formula") {
|
|
604
|
+
const embedded = block.document;
|
|
605
|
+
if (embedded.kind === "formula") out.push(embedded.formula);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function collectFormulas(document) {
|
|
610
|
+
if (document.kind !== "wordprocessing") return [];
|
|
611
|
+
const out = [];
|
|
612
|
+
for (const section of document.sections) collectFormulasFromBlocks(section.blocks, out);
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
function runCorpus(documents, options) {
|
|
616
|
+
const reports = documents.map(({ label, document }) => {
|
|
617
|
+
const symbolTable = document.symbolTable ?? {
|
|
618
|
+
symbols: [],
|
|
619
|
+
units: []
|
|
620
|
+
};
|
|
621
|
+
return {
|
|
622
|
+
label,
|
|
623
|
+
report: runWorkedExampleSequence(collectFormulas(document), symbolTable, options)
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
const matched = sumBy(reports, (r) => r.report.matched);
|
|
627
|
+
const mismatched = sumBy(reports, (r) => r.report.mismatched);
|
|
628
|
+
const gaps = sumBy(reports, (r) => r.report.gaps);
|
|
629
|
+
const unresolved = sumBy(reports, (r) => r.report.unresolved);
|
|
630
|
+
return {
|
|
631
|
+
documents: reports,
|
|
632
|
+
total: sumBy(reports, (r) => r.report.total),
|
|
633
|
+
matched,
|
|
634
|
+
mismatched,
|
|
635
|
+
gaps,
|
|
636
|
+
unresolved,
|
|
637
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function sumBy(items, project) {
|
|
641
|
+
return items.reduce((total, item) => total + project(item), 0);
|
|
642
|
+
}
|
|
643
|
+
function formatOutcome(outcome) {
|
|
644
|
+
switch (outcome.outcome) {
|
|
645
|
+
case "match": return `match: ${outcome.targetSymbol}`;
|
|
646
|
+
case "mismatch": return `MISMATCH: ${outcome.targetSymbol} -- expected ${formatEvaluationResult(outcome.expected)}, got ${formatEvaluationResult(outcome.actual)}`;
|
|
647
|
+
case "gap": return `GAP (${outcome.gap}): ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
648
|
+
case "unresolved": return `unresolved: ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function formatEvaluationResult(value) {
|
|
652
|
+
if (value.kind === "interval") return `[${value.min}, ${value.max}]`;
|
|
653
|
+
return `${value.magnitude}`;
|
|
654
|
+
}
|
|
655
|
+
function formatCorpusReport(report) {
|
|
656
|
+
const lines = [];
|
|
657
|
+
for (const { label, report: documentReport } of report.documents) {
|
|
658
|
+
const coverageText = documentReport.coverage === void 0 ? "no stated answers" : `${(documentReport.coverage * 100).toFixed(1)}% (${documentReport.matched}/${documentReport.matched + documentReport.mismatched})`;
|
|
659
|
+
lines.push(`${label}: ${coverageText}`);
|
|
660
|
+
for (const outcome of documentReport.outcomes) if (outcome.outcome !== "match") lines.push(` ${formatOutcome(outcome)}`);
|
|
661
|
+
}
|
|
662
|
+
const combinedText = report.coverage === void 0 ? "no stated answers in corpus" : `${(report.coverage * 100).toFixed(1)}% (${report.matched}/${report.matched + report.mismatched}), ${report.gaps} gap(s), ${report.unresolved} unresolved`;
|
|
663
|
+
lines.push(`TOTAL: ${combinedText}`);
|
|
664
|
+
return lines.join("\n");
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
452
667
|
exports.DivisionByZeroError = DivisionByZeroError;
|
|
453
668
|
exports.IncompatibleDimensionsError = IncompatibleDimensionsError;
|
|
454
669
|
exports.NonConvergentSolveError = NonConvergentSolveError;
|
|
@@ -461,6 +676,7 @@ exports.absQuantity = absQuantity;
|
|
|
461
676
|
exports.addIntervals = addIntervals;
|
|
462
677
|
exports.addQuantities = addQuantities;
|
|
463
678
|
exports.addRational = addRational;
|
|
679
|
+
exports.collectFormulas = collectFormulas;
|
|
464
680
|
exports.cosQuantity = cosQuantity;
|
|
465
681
|
exports.dimensionExponent = dimensionExponent;
|
|
466
682
|
exports.dimensionToString = dimensionToString;
|
|
@@ -470,6 +686,7 @@ exports.divideIntervals = divideIntervals;
|
|
|
470
686
|
exports.divideQuantities = divideQuantities;
|
|
471
687
|
exports.divideRational = divideRational;
|
|
472
688
|
exports.evaluate = evaluate;
|
|
689
|
+
exports.formatCorpusReport = formatCorpusReport;
|
|
473
690
|
exports.interval = interval;
|
|
474
691
|
exports.isDimensionless = isDimensionless;
|
|
475
692
|
exports.isInterval = isInterval;
|
|
@@ -483,6 +700,8 @@ exports.pointInterval = pointInterval;
|
|
|
483
700
|
exports.powQuantity = powQuantity;
|
|
484
701
|
exports.quantity = quantity;
|
|
485
702
|
exports.rationalToNumber = rationalToNumber;
|
|
703
|
+
exports.runCorpus = runCorpus;
|
|
704
|
+
exports.runWorkedExampleSequence = runWorkedExampleSequence;
|
|
486
705
|
exports.scaleDimension = scaleDimension;
|
|
487
706
|
exports.sinQuantity = sinQuantity;
|
|
488
707
|
exports.solveFor = solveFor;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
1
|
+
import { ContentDocument, ContentFormula, DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
2
2
|
//#region src/compute/rational.d.ts
|
|
3
3
|
interface Rational {
|
|
4
4
|
readonly n: bigint;
|
|
@@ -49,9 +49,9 @@ declare class NumericDomainError extends Error {
|
|
|
49
49
|
constructor(operation: string, detail: string);
|
|
50
50
|
}
|
|
51
51
|
declare class NonConvergentSolveError extends Error {
|
|
52
|
-
readonly method:
|
|
52
|
+
readonly method: "bisection" | "newton";
|
|
53
53
|
readonly iterations: number;
|
|
54
|
-
constructor(method:
|
|
54
|
+
constructor(method: "bisection" | "newton", iterations: number, detail: string);
|
|
55
55
|
}
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/compute/quantity.d.ts
|
|
@@ -84,7 +84,7 @@ declare function isInterval(value: EvaluationResult): value is Interval;
|
|
|
84
84
|
declare function evaluate(expression: MathExpression, bindings: FormulaBindings, context?: SymbolTable): EvaluationResult;
|
|
85
85
|
//#endregion
|
|
86
86
|
//#region src/compute/solve.d.ts
|
|
87
|
-
type SolveMethod =
|
|
87
|
+
type SolveMethod = "bisection" | "newton";
|
|
88
88
|
interface SolveForOptions {
|
|
89
89
|
/** Which root-finding algorithm to use. Default: 'bisection' (needs no derivative and cannot diverge the way Newton can, so it is the safer default; Newton converges faster once it has a decent initialGuess). */
|
|
90
90
|
method?: SolveMethod;
|
|
@@ -103,4 +103,66 @@ interface SolveForOptions {
|
|
|
103
103
|
}
|
|
104
104
|
declare function solveFor(expression: MathExpression, targetValue: number, unknownSymbol: string, bindings: FormulaBindings, options?: SolveForOptions, context?: SymbolTable): number;
|
|
105
105
|
//#endregion
|
|
106
|
-
|
|
106
|
+
//#region src/harness/worked-example.d.ts
|
|
107
|
+
type WorkedExampleGap = "unbound-symbol" | "unknown-unit" | "incompatible-dimensions" | "division-by-zero" | "unsupported-construct" | "numeric-domain" | "non-convergent-solve" | "other-evaluation-error";
|
|
108
|
+
interface WorkedExampleMatch {
|
|
109
|
+
readonly outcome: "match";
|
|
110
|
+
readonly targetSymbol: string;
|
|
111
|
+
readonly expected: Quantity;
|
|
112
|
+
readonly actual: Quantity;
|
|
113
|
+
}
|
|
114
|
+
interface WorkedExampleMismatch {
|
|
115
|
+
readonly outcome: "mismatch";
|
|
116
|
+
readonly targetSymbol: string;
|
|
117
|
+
readonly expected: Quantity;
|
|
118
|
+
readonly actual: Quantity;
|
|
119
|
+
}
|
|
120
|
+
interface WorkedExampleGapResult {
|
|
121
|
+
readonly outcome: "gap";
|
|
122
|
+
readonly gap: WorkedExampleGap;
|
|
123
|
+
readonly targetSymbol: string;
|
|
124
|
+
readonly message: string;
|
|
125
|
+
}
|
|
126
|
+
interface WorkedExampleUnresolved {
|
|
127
|
+
readonly outcome: "unresolved";
|
|
128
|
+
readonly targetSymbol: string;
|
|
129
|
+
readonly message: string;
|
|
130
|
+
}
|
|
131
|
+
type WorkedExampleOutcome = WorkedExampleMatch | WorkedExampleMismatch | WorkedExampleGapResult | WorkedExampleUnresolved;
|
|
132
|
+
interface WorkedExampleReport {
|
|
133
|
+
readonly outcomes: readonly WorkedExampleOutcome[];
|
|
134
|
+
readonly total: number;
|
|
135
|
+
readonly matched: number;
|
|
136
|
+
readonly mismatched: number;
|
|
137
|
+
readonly gaps: number;
|
|
138
|
+
readonly unresolved: number;
|
|
139
|
+
readonly coverage: number | undefined;
|
|
140
|
+
}
|
|
141
|
+
interface WorkedExampleOptions {
|
|
142
|
+
readonly relativeTolerance?: number;
|
|
143
|
+
}
|
|
144
|
+
declare function runWorkedExampleSequence(formulas: readonly ContentFormula[], symbolTable?: SymbolTable, options?: WorkedExampleOptions): WorkedExampleReport;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/harness/corpus.d.ts
|
|
147
|
+
declare function collectFormulas(document: ContentDocument): readonly ContentFormula[];
|
|
148
|
+
interface CorpusDocument {
|
|
149
|
+
readonly label: string;
|
|
150
|
+
readonly document: ContentDocument;
|
|
151
|
+
}
|
|
152
|
+
interface CorpusDocumentReport {
|
|
153
|
+
readonly label: string;
|
|
154
|
+
readonly report: WorkedExampleReport;
|
|
155
|
+
}
|
|
156
|
+
interface CorpusReport {
|
|
157
|
+
readonly documents: readonly CorpusDocumentReport[];
|
|
158
|
+
readonly total: number;
|
|
159
|
+
readonly matched: number;
|
|
160
|
+
readonly mismatched: number;
|
|
161
|
+
readonly gaps: number;
|
|
162
|
+
readonly unresolved: number;
|
|
163
|
+
readonly coverage: number | undefined;
|
|
164
|
+
}
|
|
165
|
+
declare function runCorpus(documents: readonly CorpusDocument[], options?: WorkedExampleOptions): CorpusReport;
|
|
166
|
+
declare function formatCorpusReport(report: CorpusReport): string;
|
|
167
|
+
//#endregion
|
|
168
|
+
export { CorpusDocument, CorpusDocumentReport, CorpusReport, DivisionByZeroError, EvaluationResult, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, Rational, SolveForOptions, SolveMethod, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, WorkedExampleGap, WorkedExampleGapResult, WorkedExampleMatch, WorkedExampleMismatch, WorkedExampleOptions, WorkedExampleOutcome, WorkedExampleReport, WorkedExampleUnresolved, absInterval, absQuantity, addIntervals, addQuantities, addRational, collectFormulas, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, formatCorpusReport, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, runCorpus, runWorkedExampleSequence, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
1
|
+
import { ContentDocument, ContentFormula, DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
2
2
|
//#region src/compute/rational.d.ts
|
|
3
3
|
interface Rational {
|
|
4
4
|
readonly n: bigint;
|
|
@@ -49,9 +49,9 @@ declare class NumericDomainError extends Error {
|
|
|
49
49
|
constructor(operation: string, detail: string);
|
|
50
50
|
}
|
|
51
51
|
declare class NonConvergentSolveError extends Error {
|
|
52
|
-
readonly method:
|
|
52
|
+
readonly method: "bisection" | "newton";
|
|
53
53
|
readonly iterations: number;
|
|
54
|
-
constructor(method:
|
|
54
|
+
constructor(method: "bisection" | "newton", iterations: number, detail: string);
|
|
55
55
|
}
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/compute/quantity.d.ts
|
|
@@ -84,7 +84,7 @@ declare function isInterval(value: EvaluationResult): value is Interval;
|
|
|
84
84
|
declare function evaluate(expression: MathExpression, bindings: FormulaBindings, context?: SymbolTable): EvaluationResult;
|
|
85
85
|
//#endregion
|
|
86
86
|
//#region src/compute/solve.d.ts
|
|
87
|
-
type SolveMethod =
|
|
87
|
+
type SolveMethod = "bisection" | "newton";
|
|
88
88
|
interface SolveForOptions {
|
|
89
89
|
/** Which root-finding algorithm to use. Default: 'bisection' (needs no derivative and cannot diverge the way Newton can, so it is the safer default; Newton converges faster once it has a decent initialGuess). */
|
|
90
90
|
method?: SolveMethod;
|
|
@@ -103,4 +103,66 @@ interface SolveForOptions {
|
|
|
103
103
|
}
|
|
104
104
|
declare function solveFor(expression: MathExpression, targetValue: number, unknownSymbol: string, bindings: FormulaBindings, options?: SolveForOptions, context?: SymbolTable): number;
|
|
105
105
|
//#endregion
|
|
106
|
-
|
|
106
|
+
//#region src/harness/worked-example.d.ts
|
|
107
|
+
type WorkedExampleGap = "unbound-symbol" | "unknown-unit" | "incompatible-dimensions" | "division-by-zero" | "unsupported-construct" | "numeric-domain" | "non-convergent-solve" | "other-evaluation-error";
|
|
108
|
+
interface WorkedExampleMatch {
|
|
109
|
+
readonly outcome: "match";
|
|
110
|
+
readonly targetSymbol: string;
|
|
111
|
+
readonly expected: Quantity;
|
|
112
|
+
readonly actual: Quantity;
|
|
113
|
+
}
|
|
114
|
+
interface WorkedExampleMismatch {
|
|
115
|
+
readonly outcome: "mismatch";
|
|
116
|
+
readonly targetSymbol: string;
|
|
117
|
+
readonly expected: Quantity;
|
|
118
|
+
readonly actual: Quantity;
|
|
119
|
+
}
|
|
120
|
+
interface WorkedExampleGapResult {
|
|
121
|
+
readonly outcome: "gap";
|
|
122
|
+
readonly gap: WorkedExampleGap;
|
|
123
|
+
readonly targetSymbol: string;
|
|
124
|
+
readonly message: string;
|
|
125
|
+
}
|
|
126
|
+
interface WorkedExampleUnresolved {
|
|
127
|
+
readonly outcome: "unresolved";
|
|
128
|
+
readonly targetSymbol: string;
|
|
129
|
+
readonly message: string;
|
|
130
|
+
}
|
|
131
|
+
type WorkedExampleOutcome = WorkedExampleMatch | WorkedExampleMismatch | WorkedExampleGapResult | WorkedExampleUnresolved;
|
|
132
|
+
interface WorkedExampleReport {
|
|
133
|
+
readonly outcomes: readonly WorkedExampleOutcome[];
|
|
134
|
+
readonly total: number;
|
|
135
|
+
readonly matched: number;
|
|
136
|
+
readonly mismatched: number;
|
|
137
|
+
readonly gaps: number;
|
|
138
|
+
readonly unresolved: number;
|
|
139
|
+
readonly coverage: number | undefined;
|
|
140
|
+
}
|
|
141
|
+
interface WorkedExampleOptions {
|
|
142
|
+
readonly relativeTolerance?: number;
|
|
143
|
+
}
|
|
144
|
+
declare function runWorkedExampleSequence(formulas: readonly ContentFormula[], symbolTable?: SymbolTable, options?: WorkedExampleOptions): WorkedExampleReport;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/harness/corpus.d.ts
|
|
147
|
+
declare function collectFormulas(document: ContentDocument): readonly ContentFormula[];
|
|
148
|
+
interface CorpusDocument {
|
|
149
|
+
readonly label: string;
|
|
150
|
+
readonly document: ContentDocument;
|
|
151
|
+
}
|
|
152
|
+
interface CorpusDocumentReport {
|
|
153
|
+
readonly label: string;
|
|
154
|
+
readonly report: WorkedExampleReport;
|
|
155
|
+
}
|
|
156
|
+
interface CorpusReport {
|
|
157
|
+
readonly documents: readonly CorpusDocumentReport[];
|
|
158
|
+
readonly total: number;
|
|
159
|
+
readonly matched: number;
|
|
160
|
+
readonly mismatched: number;
|
|
161
|
+
readonly gaps: number;
|
|
162
|
+
readonly unresolved: number;
|
|
163
|
+
readonly coverage: number | undefined;
|
|
164
|
+
}
|
|
165
|
+
declare function runCorpus(documents: readonly CorpusDocument[], options?: WorkedExampleOptions): CorpusReport;
|
|
166
|
+
declare function formatCorpusReport(report: CorpusReport): string;
|
|
167
|
+
//#endregion
|
|
168
|
+
export { CorpusDocument, CorpusDocumentReport, CorpusReport, DivisionByZeroError, EvaluationResult, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, Rational, SolveForOptions, SolveMethod, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, WorkedExampleGap, WorkedExampleGapResult, WorkedExampleMatch, WorkedExampleMismatch, WorkedExampleOptions, WorkedExampleOutcome, WorkedExampleReport, WorkedExampleUnresolved, absInterval, absQuantity, addIntervals, addQuantities, addRational, collectFormulas, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, formatCorpusReport, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, runCorpus, runWorkedExampleSequence, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
|
package/dist/index.js
CHANGED
|
@@ -276,7 +276,7 @@ function absInterval(a) {
|
|
|
276
276
|
}
|
|
277
277
|
//#endregion
|
|
278
278
|
//#region src/compute/evaluate.ts
|
|
279
|
-
const EMPTY_SYMBOL_TABLE$
|
|
279
|
+
const EMPTY_SYMBOL_TABLE$2 = {
|
|
280
280
|
symbols: [],
|
|
281
281
|
units: []
|
|
282
282
|
};
|
|
@@ -286,7 +286,7 @@ function isInterval(value) {
|
|
|
286
286
|
function toInterval(value) {
|
|
287
287
|
return isInterval(value) ? value : pointInterval(value.magnitude, value.dimension);
|
|
288
288
|
}
|
|
289
|
-
function asQuantity(value, context) {
|
|
289
|
+
function asQuantity$1(value, context) {
|
|
290
290
|
if (isInterval(value)) throw new UnsupportedExpressionError(context, "this position requires a plain Quantity, not an Interval");
|
|
291
291
|
return value;
|
|
292
292
|
}
|
|
@@ -322,7 +322,7 @@ const UNARY_OPERATORS = {
|
|
|
322
322
|
"math:cos": { quantity: cosQuantity },
|
|
323
323
|
"math:tan": { quantity: tanQuantity }
|
|
324
324
|
};
|
|
325
|
-
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$
|
|
325
|
+
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$2) {
|
|
326
326
|
switch (expression.kind) {
|
|
327
327
|
case "num": return quantity(rationalToNumber(toRational(expression)), {});
|
|
328
328
|
case "qty": return evaluateQty(expression, context);
|
|
@@ -345,19 +345,27 @@ function evaluateQty(node, context) {
|
|
|
345
345
|
if (unit.offsetToSi !== void 0) siValue = addRational(siValue, toRational(unit.offsetToSi));
|
|
346
346
|
return quantity(rationalToNumber(siValue), unit.dimension);
|
|
347
347
|
}
|
|
348
|
+
function expectTwoArgs(args, subject) {
|
|
349
|
+
const [left, right] = args;
|
|
350
|
+
if (args.length !== 2 || left === void 0 || right === void 0) throw new UnsupportedExpressionError("evaluate", `${subject} takes exactly 2 arguments, got ${args.length}`);
|
|
351
|
+
return [left, right];
|
|
352
|
+
}
|
|
353
|
+
function expectOneArg(args, subject) {
|
|
354
|
+
const [only] = args;
|
|
355
|
+
if (args.length !== 1 || only === void 0) throw new UnsupportedExpressionError("evaluate", `${subject} takes exactly 1 argument, got ${args.length}`);
|
|
356
|
+
return only;
|
|
357
|
+
}
|
|
348
358
|
function evaluateApp(node, bindings, context) {
|
|
349
359
|
const args = node.args.map((arg) => evaluate(arg, bindings, context));
|
|
350
360
|
const binary = BINARY_OPERATORS[node.operator];
|
|
351
361
|
if (binary !== void 0) {
|
|
352
|
-
|
|
353
|
-
const [left, right] = args;
|
|
362
|
+
const [left, right] = expectTwoArgs(args, `operator '${node.operator}'`);
|
|
354
363
|
if (isInterval(left) || isInterval(right)) return binary.interval(toInterval(left), toInterval(right));
|
|
355
364
|
return binary.quantity(left, right);
|
|
356
365
|
}
|
|
357
366
|
const unary = UNARY_OPERATORS[node.operator];
|
|
358
367
|
if (unary !== void 0) {
|
|
359
|
-
|
|
360
|
-
const [only] = args;
|
|
368
|
+
const only = expectOneArg(args, `operator '${node.operator}'`);
|
|
361
369
|
if (isInterval(only)) {
|
|
362
370
|
if (unary.interval === void 0) throw new UnsupportedExpressionError("evaluate", `operator '${node.operator}' has no interval rule in this pass`);
|
|
363
371
|
return unary.interval(only);
|
|
@@ -365,15 +373,14 @@ function evaluateApp(node, bindings, context) {
|
|
|
365
373
|
return unary.quantity(only);
|
|
366
374
|
}
|
|
367
375
|
if (node.operator === "math:pow") {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return powQuantity(asQuantity(base, "evaluate"), asQuantity(exponent, "evaluate"));
|
|
376
|
+
const [base, exponent] = expectTwoArgs(args, "'math:pow'");
|
|
377
|
+
return powQuantity(asQuantity$1(base, "evaluate"), asQuantity$1(exponent, "evaluate"));
|
|
371
378
|
}
|
|
372
379
|
throw new UnsupportedExpressionError("evaluate", `unknown operator '${node.operator}'`);
|
|
373
380
|
}
|
|
374
381
|
function evaluateBinder(node, bindings, context) {
|
|
375
|
-
const lower = asQuantity(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
376
|
-
const upper = asQuantity(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
382
|
+
const lower = asQuantity$1(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
383
|
+
const upper = asQuantity$1(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
377
384
|
if (!isDimensionless(lower.dimension) || !isDimensionless(upper.dimension)) throw new IncompatibleDimensionsError(`math:${node.kind}`, lower.dimension, upper.dimension, "binder bounds must be dimensionless");
|
|
378
385
|
if (!Number.isInteger(lower.magnitude) || !Number.isInteger(upper.magnitude)) throw new UnsupportedExpressionError(`evaluate:${node.kind}`, "binder bounds must evaluate to integers");
|
|
379
386
|
let accumulator = node.kind === "sum" ? quantity(0, {}) : quantity(1, {});
|
|
@@ -382,7 +389,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
382
389
|
...bindings,
|
|
383
390
|
[node.binder]: quantity(i, {})
|
|
384
391
|
};
|
|
385
|
-
const bodyValue = asQuantity(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
392
|
+
const bodyValue = asQuantity$1(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
386
393
|
accumulator = node.kind === "sum" ? addQuantities(accumulator, bodyValue) : multiplyQuantities(accumulator, bodyValue);
|
|
387
394
|
}
|
|
388
395
|
return accumulator;
|
|
@@ -392,7 +399,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
392
399
|
const DEFAULT_TOLERANCE = 1e-9;
|
|
393
400
|
const DEFAULT_MAX_ITERATIONS = 100;
|
|
394
401
|
const DEFAULT_DERIVATIVE_STEP = 1e-6;
|
|
395
|
-
const EMPTY_SYMBOL_TABLE = {
|
|
402
|
+
const EMPTY_SYMBOL_TABLE$1 = {
|
|
396
403
|
symbols: [],
|
|
397
404
|
units: []
|
|
398
405
|
};
|
|
@@ -406,7 +413,7 @@ function residualFn(expression, targetValue, unknownSymbol, bindings, context, d
|
|
|
406
413
|
return result.magnitude - targetValue;
|
|
407
414
|
};
|
|
408
415
|
}
|
|
409
|
-
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE) {
|
|
416
|
+
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE$1) {
|
|
410
417
|
const method = options.method ?? "bisection";
|
|
411
418
|
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
|
|
412
419
|
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
@@ -448,4 +455,212 @@ function newton(f, initialGuess, tolerance, maxIterations, h) {
|
|
|
448
455
|
throw new NonConvergentSolveError("newton", maxIterations, `residual still exceeds tolerance ${tolerance} after ${maxIterations} iterations`);
|
|
449
456
|
}
|
|
450
457
|
//#endregion
|
|
451
|
-
|
|
458
|
+
//#region src/harness/worked-example.ts
|
|
459
|
+
const DEFAULT_RELATIVE_TOLERANCE = .001;
|
|
460
|
+
const EMPTY_BINDINGS = {};
|
|
461
|
+
const EMPTY_SYMBOL_TABLE = {
|
|
462
|
+
symbols: [],
|
|
463
|
+
units: []
|
|
464
|
+
};
|
|
465
|
+
function asEquality(expression) {
|
|
466
|
+
if (expression.kind !== "app" || expression.operator !== "math:eq") return;
|
|
467
|
+
const [lhs, rhs] = expression.args;
|
|
468
|
+
if (lhs === void 0 || rhs === void 0 || lhs.kind !== "sym") return;
|
|
469
|
+
return {
|
|
470
|
+
targetSymbol: lhs.id,
|
|
471
|
+
rhs
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function containsSymbol(expression) {
|
|
475
|
+
switch (expression.kind) {
|
|
476
|
+
case "sym": return true;
|
|
477
|
+
case "num":
|
|
478
|
+
case "qty":
|
|
479
|
+
case "unparsed": return false;
|
|
480
|
+
case "app": return expression.args.some(containsSymbol);
|
|
481
|
+
case "sum":
|
|
482
|
+
case "prod": return containsSymbol(expression.lower) || containsSymbol(expression.upper) || containsSymbol(expression.body);
|
|
483
|
+
case "matrix": return expression.rows.some((row) => row.some(containsSymbol));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function asQuantity(value) {
|
|
487
|
+
if (isInterval(value)) throw new UnsupportedExpressionError("runWorkedExampleSequence", "this harness compares point-valued Quantity answers only; a symbol resolving to a range (Interval) has no stated-answer comparison defined yet");
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
function gapFromError(error) {
|
|
491
|
+
if (error instanceof UnboundSymbolError) return "unbound-symbol";
|
|
492
|
+
if (error instanceof UnknownUnitError) return "unknown-unit";
|
|
493
|
+
if (error instanceof IncompatibleDimensionsError) return "incompatible-dimensions";
|
|
494
|
+
if (error instanceof DivisionByZeroError) return "division-by-zero";
|
|
495
|
+
if (error instanceof UnsupportedExpressionError) return "unsupported-construct";
|
|
496
|
+
if (error instanceof NumericDomainError) return "numeric-domain";
|
|
497
|
+
if (error instanceof NonConvergentSolveError) return "non-convergent-solve";
|
|
498
|
+
return "other-evaluation-error";
|
|
499
|
+
}
|
|
500
|
+
function errorMessage(error) {
|
|
501
|
+
return error instanceof Error ? error.message : String(error);
|
|
502
|
+
}
|
|
503
|
+
function withinTolerance(actual, expected, relativeTolerance) {
|
|
504
|
+
if (expected === 0) return Math.abs(actual) <= relativeTolerance;
|
|
505
|
+
return Math.abs(actual - expected) / Math.abs(expected) <= relativeTolerance;
|
|
506
|
+
}
|
|
507
|
+
function resultsMatch(actual, expected, relativeTolerance) {
|
|
508
|
+
return dimensionsEqual(actual.dimension, expected.dimension) && withinTolerance(actual.magnitude, expected.magnitude, relativeTolerance);
|
|
509
|
+
}
|
|
510
|
+
function runWorkedExampleSequence(formulas, symbolTable = EMPTY_SYMBOL_TABLE, options) {
|
|
511
|
+
const relativeTolerance = options?.relativeTolerance ?? DEFAULT_RELATIVE_TOLERANCE;
|
|
512
|
+
const bindings = {};
|
|
513
|
+
const outcomes = [];
|
|
514
|
+
let pending;
|
|
515
|
+
const closeUnresolved = () => {
|
|
516
|
+
if (pending === void 0) return;
|
|
517
|
+
outcomes.push({
|
|
518
|
+
outcome: "unresolved",
|
|
519
|
+
targetSymbol: pending.targetSymbol,
|
|
520
|
+
message: `"${pending.targetSymbol}" was defined but the sequence never restated it as a closed numeric result before ending or being superseded by another definition`
|
|
521
|
+
});
|
|
522
|
+
pending = void 0;
|
|
523
|
+
};
|
|
524
|
+
for (const formula of formulas) {
|
|
525
|
+
if (formula.content === void 0) continue;
|
|
526
|
+
const equality = asEquality(formula.content);
|
|
527
|
+
if (equality === void 0) continue;
|
|
528
|
+
const { targetSymbol, rhs } = equality;
|
|
529
|
+
if (containsSymbol(rhs)) {
|
|
530
|
+
closeUnresolved();
|
|
531
|
+
pending = {
|
|
532
|
+
targetSymbol,
|
|
533
|
+
rhs
|
|
534
|
+
};
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
let closedValue;
|
|
538
|
+
try {
|
|
539
|
+
closedValue = asQuantity(evaluate(rhs, EMPTY_BINDINGS, symbolTable));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
outcomes.push({
|
|
542
|
+
outcome: "gap",
|
|
543
|
+
gap: gapFromError(error),
|
|
544
|
+
targetSymbol,
|
|
545
|
+
message: errorMessage(error)
|
|
546
|
+
});
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (pending?.targetSymbol === targetSymbol) {
|
|
550
|
+
const { rhs: definitionRhs } = pending;
|
|
551
|
+
pending = void 0;
|
|
552
|
+
let actual;
|
|
553
|
+
try {
|
|
554
|
+
actual = asQuantity(evaluate(definitionRhs, bindings, symbolTable));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
outcomes.push({
|
|
557
|
+
outcome: "gap",
|
|
558
|
+
gap: gapFromError(error),
|
|
559
|
+
targetSymbol,
|
|
560
|
+
message: errorMessage(error)
|
|
561
|
+
});
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
outcomes.push(resultsMatch(actual, closedValue, relativeTolerance) ? {
|
|
565
|
+
outcome: "match",
|
|
566
|
+
targetSymbol,
|
|
567
|
+
expected: closedValue,
|
|
568
|
+
actual
|
|
569
|
+
} : {
|
|
570
|
+
outcome: "mismatch",
|
|
571
|
+
targetSymbol,
|
|
572
|
+
expected: closedValue,
|
|
573
|
+
actual
|
|
574
|
+
});
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
bindings[targetSymbol] = closedValue;
|
|
578
|
+
}
|
|
579
|
+
closeUnresolved();
|
|
580
|
+
const matched = outcomes.filter((o) => o.outcome === "match").length;
|
|
581
|
+
const mismatched = outcomes.filter((o) => o.outcome === "mismatch").length;
|
|
582
|
+
const gaps = outcomes.filter((o) => o.outcome === "gap").length;
|
|
583
|
+
const unresolved = outcomes.filter((o) => o.outcome === "unresolved").length;
|
|
584
|
+
return {
|
|
585
|
+
outcomes,
|
|
586
|
+
total: outcomes.length,
|
|
587
|
+
matched,
|
|
588
|
+
mismatched,
|
|
589
|
+
gaps,
|
|
590
|
+
unresolved,
|
|
591
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/harness/corpus.ts
|
|
596
|
+
function collectFormulasFromBlocks(blocks, out) {
|
|
597
|
+
for (const block of blocks) {
|
|
598
|
+
if (block.kind === "table") {
|
|
599
|
+
for (const row of block.rows) for (const cell of row.cells) collectFormulasFromBlocks(cell.blocks, out);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (block.kind === "embeddedObject" && block.objectKind === "formula") {
|
|
603
|
+
const embedded = block.document;
|
|
604
|
+
if (embedded.kind === "formula") out.push(embedded.formula);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function collectFormulas(document) {
|
|
609
|
+
if (document.kind !== "wordprocessing") return [];
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const section of document.sections) collectFormulasFromBlocks(section.blocks, out);
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
function runCorpus(documents, options) {
|
|
615
|
+
const reports = documents.map(({ label, document }) => {
|
|
616
|
+
const symbolTable = document.symbolTable ?? {
|
|
617
|
+
symbols: [],
|
|
618
|
+
units: []
|
|
619
|
+
};
|
|
620
|
+
return {
|
|
621
|
+
label,
|
|
622
|
+
report: runWorkedExampleSequence(collectFormulas(document), symbolTable, options)
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
const matched = sumBy(reports, (r) => r.report.matched);
|
|
626
|
+
const mismatched = sumBy(reports, (r) => r.report.mismatched);
|
|
627
|
+
const gaps = sumBy(reports, (r) => r.report.gaps);
|
|
628
|
+
const unresolved = sumBy(reports, (r) => r.report.unresolved);
|
|
629
|
+
return {
|
|
630
|
+
documents: reports,
|
|
631
|
+
total: sumBy(reports, (r) => r.report.total),
|
|
632
|
+
matched,
|
|
633
|
+
mismatched,
|
|
634
|
+
gaps,
|
|
635
|
+
unresolved,
|
|
636
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function sumBy(items, project) {
|
|
640
|
+
return items.reduce((total, item) => total + project(item), 0);
|
|
641
|
+
}
|
|
642
|
+
function formatOutcome(outcome) {
|
|
643
|
+
switch (outcome.outcome) {
|
|
644
|
+
case "match": return `match: ${outcome.targetSymbol}`;
|
|
645
|
+
case "mismatch": return `MISMATCH: ${outcome.targetSymbol} -- expected ${formatEvaluationResult(outcome.expected)}, got ${formatEvaluationResult(outcome.actual)}`;
|
|
646
|
+
case "gap": return `GAP (${outcome.gap}): ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
647
|
+
case "unresolved": return `unresolved: ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function formatEvaluationResult(value) {
|
|
651
|
+
if (value.kind === "interval") return `[${value.min}, ${value.max}]`;
|
|
652
|
+
return `${value.magnitude}`;
|
|
653
|
+
}
|
|
654
|
+
function formatCorpusReport(report) {
|
|
655
|
+
const lines = [];
|
|
656
|
+
for (const { label, report: documentReport } of report.documents) {
|
|
657
|
+
const coverageText = documentReport.coverage === void 0 ? "no stated answers" : `${(documentReport.coverage * 100).toFixed(1)}% (${documentReport.matched}/${documentReport.matched + documentReport.mismatched})`;
|
|
658
|
+
lines.push(`${label}: ${coverageText}`);
|
|
659
|
+
for (const outcome of documentReport.outcomes) if (outcome.outcome !== "match") lines.push(` ${formatOutcome(outcome)}`);
|
|
660
|
+
}
|
|
661
|
+
const combinedText = report.coverage === void 0 ? "no stated answers in corpus" : `${(report.coverage * 100).toFixed(1)}% (${report.matched}/${report.matched + report.mismatched}), ${report.gaps} gap(s), ${report.unresolved} unresolved`;
|
|
662
|
+
lines.push(`TOTAL: ${combinedText}`);
|
|
663
|
+
return lines.join("\n");
|
|
664
|
+
}
|
|
665
|
+
//#endregion
|
|
666
|
+
export { DivisionByZeroError, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, absInterval, absQuantity, addIntervals, addQuantities, addRational, collectFormulas, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, formatCorpusReport, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, runCorpus, runWorkedExampleSequence, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-compute.js",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -65,27 +65,20 @@
|
|
|
65
65
|
},
|
|
66
66
|
"packageManager": "pnpm@11.6.0",
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"document-schema.js": "^5.
|
|
68
|
+
"document-schema.js": "^5.1.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
72
72
|
"@cloudflare/vitest-pool-workers": "^0.20.1",
|
|
73
|
-
"@commitlint/cli": "^21.2.1",
|
|
74
|
-
"@commitlint/config-conventional": "^21.2.0",
|
|
75
|
-
"@eslint/js": "^10.0.1",
|
|
76
|
-
"@semantic-release/changelog": "^7.0.0",
|
|
77
|
-
"@semantic-release/git": "^11.0.1",
|
|
78
73
|
"@types/node": "^26.1.2",
|
|
74
|
+
"documents.js": "^6.1.2",
|
|
79
75
|
"eslint": "^10.8.0",
|
|
80
|
-
"globals": "^17.8.0",
|
|
81
76
|
"husky": "^9.1.7",
|
|
82
|
-
"
|
|
77
|
+
"markdown-codec": "^6.1.0",
|
|
83
78
|
"publint": "^0.3.21",
|
|
84
|
-
"semantic-release": "^25.0.8",
|
|
85
79
|
"tsdown": "^0.22.13",
|
|
86
80
|
"turbo": "^2.10.8",
|
|
87
81
|
"typescript": "^6.0.3",
|
|
88
|
-
"typescript-eslint": "^8.65.0",
|
|
89
82
|
"vitest": "^4.1.10"
|
|
90
83
|
},
|
|
91
84
|
"lint-staged": {
|