document-compute.js 1.1.0 → 1.2.1

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 CHANGED
@@ -102,9 +102,20 @@ solveFor(xSquared, 4, "x", {}, { method: "newton", initialGuess: 3 }); // 2, via
102
102
 
103
103
  ## Deviations from the issue
104
104
 
105
- Two things #573 asks for are not here in full. One was closed at adoption: `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.
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.
106
106
 
107
- The one that remains: **the worked-example test harness** the issue describes as its differentiator — measuring the fraction of a real document corpus's formulae whose evaluation reproduces the document's own stated answer. That harness needs real document corpora with stated formulae and answers, which this package has no access to build against; what ships here instead is thorough unit-level coverage of `evaluate`, the unit/dimension model, interval arithmetic, and `solveFor` in isolation (`src/compute/*.test.ts`). It is tracked as a follow-up to this package, not silently dropped.
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.
108
119
 
109
120
  ## Out of scope
110
121
 
@@ -112,7 +123,6 @@ Quoting the issue's own scope line directly: **this is deliberately not a CAS in
112
123
 
113
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.
114
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.
115
- - **The worked-example test harness** — see Deviations from the issue above.
116
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.
117
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.
118
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$1 = {
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$1) {
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);
@@ -375,13 +375,13 @@ function evaluateApp(node, bindings, context) {
375
375
  }
376
376
  if (node.operator === "math:pow") {
377
377
  const [base, exponent] = expectTwoArgs(args, "'math:pow'");
378
- return powQuantity(asQuantity(base, "evaluate"), asQuantity(exponent, "evaluate"));
378
+ return powQuantity(asQuantity$1(base, "evaluate"), asQuantity$1(exponent, "evaluate"));
379
379
  }
380
380
  throw new UnsupportedExpressionError("evaluate", `unknown operator '${node.operator}'`);
381
381
  }
382
382
  function evaluateBinder(node, bindings, context) {
383
- const lower = asQuantity(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
384
- 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}`);
385
385
  if (!isDimensionless(lower.dimension) || !isDimensionless(upper.dimension)) throw new IncompatibleDimensionsError(`math:${node.kind}`, lower.dimension, upper.dimension, "binder bounds must be dimensionless");
386
386
  if (!Number.isInteger(lower.magnitude) || !Number.isInteger(upper.magnitude)) throw new UnsupportedExpressionError(`evaluate:${node.kind}`, "binder bounds must evaluate to integers");
387
387
  let accumulator = node.kind === "sum" ? quantity(0, {}) : quantity(1, {});
@@ -390,7 +390,7 @@ function evaluateBinder(node, bindings, context) {
390
390
  ...bindings,
391
391
  [node.binder]: quantity(i, {})
392
392
  };
393
- const bodyValue = asQuantity(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
393
+ const bodyValue = asQuantity$1(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
394
394
  accumulator = node.kind === "sum" ? addQuantities(accumulator, bodyValue) : multiplyQuantities(accumulator, bodyValue);
395
395
  }
396
396
  return accumulator;
@@ -400,7 +400,7 @@ function evaluateBinder(node, bindings, context) {
400
400
  const DEFAULT_TOLERANCE = 1e-9;
401
401
  const DEFAULT_MAX_ITERATIONS = 100;
402
402
  const DEFAULT_DERIVATIVE_STEP = 1e-6;
403
- const EMPTY_SYMBOL_TABLE = {
403
+ const EMPTY_SYMBOL_TABLE$1 = {
404
404
  symbols: [],
405
405
  units: []
406
406
  };
@@ -414,7 +414,7 @@ function residualFn(expression, targetValue, unknownSymbol, bindings, context, d
414
414
  return result.magnitude - targetValue;
415
415
  };
416
416
  }
417
- function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE) {
417
+ function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE$1) {
418
418
  const method = options.method ?? "bisection";
419
419
  const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
420
420
  const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
@@ -456,6 +456,214 @@ function newton(f, initialGuess, tolerance, maxIterations, h) {
456
456
  throw new NonConvergentSolveError("newton", maxIterations, `residual still exceeds tolerance ${tolerance} after ${maxIterations} iterations`);
457
457
  }
458
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
459
667
  exports.DivisionByZeroError = DivisionByZeroError;
460
668
  exports.IncompatibleDimensionsError = IncompatibleDimensionsError;
461
669
  exports.NonConvergentSolveError = NonConvergentSolveError;
@@ -468,6 +676,7 @@ exports.absQuantity = absQuantity;
468
676
  exports.addIntervals = addIntervals;
469
677
  exports.addQuantities = addQuantities;
470
678
  exports.addRational = addRational;
679
+ exports.collectFormulas = collectFormulas;
471
680
  exports.cosQuantity = cosQuantity;
472
681
  exports.dimensionExponent = dimensionExponent;
473
682
  exports.dimensionToString = dimensionToString;
@@ -477,6 +686,7 @@ exports.divideIntervals = divideIntervals;
477
686
  exports.divideQuantities = divideQuantities;
478
687
  exports.divideRational = divideRational;
479
688
  exports.evaluate = evaluate;
689
+ exports.formatCorpusReport = formatCorpusReport;
480
690
  exports.interval = interval;
481
691
  exports.isDimensionless = isDimensionless;
482
692
  exports.isInterval = isInterval;
@@ -490,6 +700,8 @@ exports.pointInterval = pointInterval;
490
700
  exports.powQuantity = powQuantity;
491
701
  exports.quantity = quantity;
492
702
  exports.rationalToNumber = rationalToNumber;
703
+ exports.runCorpus = runCorpus;
704
+ exports.runWorkedExampleSequence = runWorkedExampleSequence;
493
705
  exports.scaleDimension = scaleDimension;
494
706
  exports.sinQuantity = sinQuantity;
495
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;
@@ -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
- export { DivisionByZeroError, EvaluationResult, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, Rational, SolveForOptions, SolveMethod, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, absInterval, absQuantity, addIntervals, addQuantities, addRational, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
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;
@@ -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
- export { DivisionByZeroError, EvaluationResult, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, Rational, SolveForOptions, SolveMethod, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, absInterval, absQuantity, addIntervals, addQuantities, addRational, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
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$1 = {
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$1) {
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);
@@ -374,13 +374,13 @@ function evaluateApp(node, bindings, context) {
374
374
  }
375
375
  if (node.operator === "math:pow") {
376
376
  const [base, exponent] = expectTwoArgs(args, "'math:pow'");
377
- return powQuantity(asQuantity(base, "evaluate"), asQuantity(exponent, "evaluate"));
377
+ return powQuantity(asQuantity$1(base, "evaluate"), asQuantity$1(exponent, "evaluate"));
378
378
  }
379
379
  throw new UnsupportedExpressionError("evaluate", `unknown operator '${node.operator}'`);
380
380
  }
381
381
  function evaluateBinder(node, bindings, context) {
382
- const lower = asQuantity(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
383
- 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}`);
384
384
  if (!isDimensionless(lower.dimension) || !isDimensionless(upper.dimension)) throw new IncompatibleDimensionsError(`math:${node.kind}`, lower.dimension, upper.dimension, "binder bounds must be dimensionless");
385
385
  if (!Number.isInteger(lower.magnitude) || !Number.isInteger(upper.magnitude)) throw new UnsupportedExpressionError(`evaluate:${node.kind}`, "binder bounds must evaluate to integers");
386
386
  let accumulator = node.kind === "sum" ? quantity(0, {}) : quantity(1, {});
@@ -389,7 +389,7 @@ function evaluateBinder(node, bindings, context) {
389
389
  ...bindings,
390
390
  [node.binder]: quantity(i, {})
391
391
  };
392
- const bodyValue = asQuantity(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
392
+ const bodyValue = asQuantity$1(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
393
393
  accumulator = node.kind === "sum" ? addQuantities(accumulator, bodyValue) : multiplyQuantities(accumulator, bodyValue);
394
394
  }
395
395
  return accumulator;
@@ -399,7 +399,7 @@ function evaluateBinder(node, bindings, context) {
399
399
  const DEFAULT_TOLERANCE = 1e-9;
400
400
  const DEFAULT_MAX_ITERATIONS = 100;
401
401
  const DEFAULT_DERIVATIVE_STEP = 1e-6;
402
- const EMPTY_SYMBOL_TABLE = {
402
+ const EMPTY_SYMBOL_TABLE$1 = {
403
403
  symbols: [],
404
404
  units: []
405
405
  };
@@ -413,7 +413,7 @@ function residualFn(expression, targetValue, unknownSymbol, bindings, context, d
413
413
  return result.magnitude - targetValue;
414
414
  };
415
415
  }
416
- function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE) {
416
+ function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE$1) {
417
417
  const method = options.method ?? "bisection";
418
418
  const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
419
419
  const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
@@ -455,4 +455,212 @@ function newton(f, initialGuess, tolerance, maxIterations, h) {
455
455
  throw new NonConvergentSolveError("newton", maxIterations, `residual still exceeds tolerance ${tolerance} after ${maxIterations} iterations`);
456
456
  }
457
457
  //#endregion
458
- export { DivisionByZeroError, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, absInterval, absQuantity, addIntervals, addQuantities, addRational, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
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.1.0",
3
+ "version": "1.2.1",
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,14 +65,16 @@
65
65
  },
66
66
  "packageManager": "pnpm@11.6.0",
67
67
  "dependencies": {
68
- "document-schema.js": "^5.1.0"
68
+ "document-schema.js": "^5.2.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@arethetypeswrong/cli": "^0.18.5",
72
72
  "@cloudflare/vitest-pool-workers": "^0.20.1",
73
73
  "@types/node": "^26.1.2",
74
+ "documents.js": "^6.1.3",
74
75
  "eslint": "^10.8.0",
75
76
  "husky": "^9.1.7",
77
+ "markdown-codec": "^6.1.1",
76
78
  "publint": "^0.3.21",
77
79
  "tsdown": "^0.22.13",
78
80
  "turbo": "^2.10.8",