Skip to content

ADR 002 Gate 1222 Monaco MathJS Security Parity

Paulo Gomes da Cruz Junior edited this page Sep 11, 2026 · 2 revisions

ADR 002: Gate 1222 — Monaco + mathjs Security and Parity Verdict

Status

APPROVED WITH CONDITIONS — Do not install in repo yet. All evaluation ran in isolated temp sandboxes only.

Gate Summary

Gate spike/1222-monacomathjs-security-gate (issue #1222) blocks #1223. It depends on two unmerged prerequisites:

  • #1214 — 10 canonical calculation fixtures: INTERIOR DRY/TRANS/WET, COAST IMMATURE/MATURE, HELI, DISPERSED 0.4, RESIDUAL edges (p52–54)
  • #1221 — formula engine is missing in repo

No canonical fixtures or engine exist in main yet. This gate pins candidate library versions, runs six evaluation phases in temp sandboxes, and grants a conditional approval for #1223 to proceed under strict constraints. Repo frontend/package.json and package-lock.json remain untouched.

Candidate versions pinned:

  • @monaco-editor/react 4.7.0 (2025-02-13) + monaco-editor 0.52.2 (peer >=0.25.0 <1, MIT)
  • mathjs 15.2.0 (2026-04-07, Apache-2.0)

Do not run npm install for these candidates in the repo. Use /tmp/gate-1222 and /tmp/gate-1222-probe only until conditions are met.

Isolation Strategy

All evidence was collected outside the repo.

  • Candidate sandbox: /tmp/gate-1222 — package.json lists only @monaco-editor/react@4.7.0, monaco-editor@0.52.2, mathjs@15.2.0 with 15 prod deps; npm audit and osv-scanner ran against this isolated lockfile.
  • Vite probe sandbox: /tmp/gate-1222-probe — same candidates plus vite@8.0.0 to test bundling and to surface dev-time transitive vulns without polluting the baseline audit.
  • Baseline sandbox: /tmp/gate-baseline — Vite 8 build without candidates, to measure gzip delta.

No files were written to frontend/node_modules or committed.

Six-Phase Evaluation

Phase Verdict Evidence (read verbatim, do not guess)
1. Security — npm audit (candidate-only) PASS — 0 vulns artifacts/audit-candidates.json + artifacts/audit-candidates.pretty.json: auditReportVersion 2, vulnerabilities: {}, prod 15 / total 17, all severities 0
Security — npm audit (probe with vite@8.0.0) PASS with note — 3 vulns, dev-time only artifacts/audit-probe.json: vite high (GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583, plus GHSA-v6wh-96g9-6wx3, GHSA-fx2h-pf6j-xcff) range >=8.0.0 <=8.0.15, fix 8.2.2; dompurify moderate/low via monaco-editor (GHSA-c2j3-45gr-mqc4, GHSA-cmwh-pvxp-8882, GHSA-vxr8-fq34-vvx9, GHSA-55q2-fjhq-7xh7, range <=3.4.12); monaco-editor flagged via dompurify >=0.54.0-dev-20250909 — not our pinned 0.52.2
Security — OSV PASS — 0 results artifacts/osv.json: walked /tmp/gate-1222/package-lock.json, found 17 packages, results: []
Transitive vuln note Informational artifacts/transitive-vuln-note.json: candidate-only 0 vulns; probe 3 vulns; osv 0; action is to pin monaco-editor 0.52.2 with dompurify >=3.4.13 override and upgrade repo vite to 8.2.2
2. License PASS — MIT + Apache-2.0 artifacts/licenses.json (license-checker production): @monaco-editor/react 4.7.0 MIT, @monaco-editor/loader 1.7.0 MIT, monaco-editor 0.52.2 MIT, mathjs 15.2.0 Apache-2.0, decimal.js 10.6.0 MIT, complex.js 2.4.3 MIT, fraction.js 5.3.4 MIT, typed-function 4.2.2 MIT, etc. No GPL/AGPL
3. Maintenance PASS with note artifacts/maintenance.json: @monaco-editor/react 4.7.0 published 2025-02-13, next RC 4.8.0-rc.3 2025-11-21, repo suren-atoyan/monaco-react 21 open issues / 0 critical-or-bug, 4736 stars, not archived; mathjs 15.2.0 published 2026-04-07, repo josdejong/mathjs 203 open issues / 46 bug / 0 critical, 15070 stars, not archived
4. Bundle CONDITIONAL PASS — measurable cost, must chunk + lazy-load artifacts/bundle-report.json (Vite 8 probe): mathjs 658,048 raw / 180,740 gz (176.5 kB), monaco_wrapper 21,920 raw / 7,520 gz (7.3 kB), react-vendor 186,470 raw / 58,700 gz, app 1,146 raw; total dist 864,000 bytes; delta +185,409 gz (+181.1 kB) vs baseline 59,542 gz (60k); note: @monaco-editor/react loader uses CDN — static bundling of monaco-editor would add ~1.8 MB raw (~400 kB gz)
5. Bundler / SSR PASS with constraint — ESM only, browser-only Monaco artifacts/npm-view-mathjs.json: type: module, module: ./lib/esm, exports . import ./lib/esm/index.js, main ./lib/cjs, sideEffects: false; artifacts/npm-view-monaco.json: @monaco-editor/react 4.7.0 module dist/index.mjs, main dist/index.js, peer monaco-editor >=0.25.0 <1; artifacts/bundle-report.json build status success on Vite 8 ESM; no fs/path in runtime; Monaco must load lazily on client only — never import monaco-editor at SSR/top-level (window-dependent)
6. Parity — rounding + fixtures CONDITIONAL PASS — BigNumber required, re-run on real fixtures artifacts/parity-report.json + artifacts/parity.log + artifacts/parity-harness.mjs + artifacts/parity-harness-v2.mjs: config create(all, {number:'BigNumber', precision:64}), rounding HALF_UP to 3 decimals via math.round(BigNumber, 3) matching Java BigDecimal; 7/7 rounding PASS, 10/10 fixture rounding PASS (synthetic fixtures covering all #1214 categories), Number vs BigNumber divergence proven; full formula parity awaits #1214 + #1221

Detailed findings for each phase follow.

1. Security — Detail

  • Isolated candidate audit is clean. Probe audit shows vite dev-time highs fixed in 8.2.2 and dompurify moderates via monaco-editor 0.56.0 (not our pinned 0.52.2). OSV confirms 0. Pin vite 8.2.2 in repo and add dompurify >=3.4.13 override if bundling monaco statically — see artifacts/transitive-vuln-note.json.

2. License — Detail

  • All production deps are MIT except mathjs Apache-2.0. Both are permissive and compatible with BC Gov private-cloud delivery. Verify artifacts/licenses.json for full list; no copyleft.

3. Maintenance — Detail

  • Both candidates are actively maintained. mathjs 15.2.0 is 19 days old at evaluation (2026-04-07). @monaco-editor/react 4.7.0 is 6 months old (2025-02-13) with a fresh 4.8.0-rc.3 signal (2025-11-21). No archived repos, no critical issues.

4. Bundle — Detail

  • mathjs is the dominant cost (~181 kB gz). Acceptable only with code-splitting. Do not bundle monaco-editor statically unless the CDN loader is rejected — that path costs another ~400 kB gz.

5. Bundler / SSR — Detail

  • Both libraries expose ESM entry points consumed by Vite 8 without plugin workarounds. mathjs import is lib/esm/index.js; @monaco-editor/react is dist/index.mjs. Neither pulls Node fs/path at runtime. Monaco editors must be dynamic-imported behind a client check (typeof window !== 'undefined') to avoid SSR document is not defined / self is not defined failures.

6. Parity — Detail

  • Synthetic harness covers 10 fixtures across required categories:

    • INTERIOR DRY_BELT, INTERIOR TRANSITION, INTERIOR WET_BELT, INTERIOR residual, COAST IMMATURE, COAST MATURE, COAST HELI, RESIDUAL EDGE x2, DISPERSED 0.4
  • Rounding rule HALF_UP verified at 7 edge cases (including 1.005 -> 1.01 at 2 dp, 4967.82315 -> 4967.823 at 3 dp). Fixture rounding parity held 10/10 when rounding unroundedTotalM3 -> roundedTotalM3 with math.round(math.bignumber(v), 3).

  • Number mode diverges on 0.1+0.2 (0.30000000000000004 vs 0.3), 100/3*3 (100 vs 99.999...), and 4967.8231+0.00005 (4967.823149999999 vs 4967.82315). BigNumber is mandatory for finance. Full formula-evaluation parity against backend BigDecimal can only be proven once real #1214 workbook fixtures and #1221 engine are available — see residual note.

Bundler Compatibility

  • Vite 8: ESM works. Verified entries: mathjs lib/esm/index.js / dist/index.mjs (see artifacts/npm-view-mathjs.json exports / module); @monaco-editor/react dist/index.mjs (see artifacts/npm-view-monaco.json). No extra Vite optimizeDeps plugin needed beyond default ESM.
  • No Node built-ins in runtime: audit of artifacts/licenses.json and bundle report shows no fs/path in client chunk.
  • Monaco browser-only: monaco-editor accesses window/document at init. Requirement: lazy-load @monaco-editor/react with React.lazy or dynamic import() inside useEffect, gate on client, never at module top-level or in SSR.

Bundle Impact and Chunking Recommendation

From artifacts/bundle-report.json Vite 8 probe:

  • Baseline (no candidates): 59,542 gz bytes
  • With candidates: +185,409 gz bytes (181.1 kB)
  • Breakout: mathjs 180.7 kB gz, monaco_wrapper 7.3 kB gz, react-vendor 57.3 kB gz

If you switch from CDN loader to static monaco-editor, add ~400 kB gz (~1.8 MB raw) — avoid unless offline bundle is required.

Recommended vite.config.ts chunking (do not copy as exact bytes — adapt to current config):

// vite.config.ts — build.rollupOptions
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          "react-vendor": ["react", "react-dom"],
          mathjs: ["mathjs"],
          monaco: ["@monaco-editor/react"],
        },
      },
    },
  },
  optimizeDeps: {
    include: ["mathjs", "@monaco-editor/react"],
  },
});

Load Monaco lazily:

const MonacoEditor = React.lazy(() => import("@monaco-editor/react"));
// render only after mount / typeof window !== 'undefined'

Do not put monaco or mathjs in the main entry chunk. Preload mathjs only on the formula-edit route if possible.

Parity Harness — Required Configuration

Constraint: NEVER use mathjs Number mode for finance.

Required creation (from artifacts/parity-harness.mjs):

import { create, all } from "mathjs";
const math = create(all, { number: "BigNumber", precision: 64 });

function canonicalRound(value) {
  return Number(math.round(math.bignumber(value), 3).toString());
}

Do not use any of these patterns:

// WRONG — introduces float error
import { evaluate, round } from "mathjs";
const mathNumber = create(all, { number: "number" });
mathNumber.evaluate("0.1 + 0.2"); // 0.30000000000000004
mathNumber.round(1.005, 2); // fragile for HALF_UP edge cases without BigNumber

Correct vs incorrect verified in artifacts/parity.log:

  • 0.1 + 0.2 Number 0.30000000000000004 vs BigNumber 0.3
  • 2.675 -> 2dp BigNumber 2.68 PASS, Number 2.67 FAIL in JS toFixed parity cases (see artifacts/parity-harness-v2.mjs)
  • 4967.82315 -> 3dp BigNumber 4967.823 matches Java HALF_UP

When #1214 fixtures land, re-run:

node /tmp/gate-1222/parity-harness.mjs
node /tmp/gate-1222/parity-harness-v2.mjs

and diff each fixture math.evaluate(formula) unrounded and canonicalRound(unrounded) against the engine output from #1221. All diffs must be 0 at 3 dp — synthetic harness tolerance 0.0005 does not apply to real fixtures.

Residual Responsibility

This verdict is synthetic. Follow-up is required when prerequisites land:

  1. When #1214 fixtures land — replace the 10 synthetic fixtures in artifacts/parity-harness.mjs with the workbook canonicals (Provincial Calculator v1.1 Interior/Coast sheets, dispersed 0.4 factor, heli multipliers, residual allocation p52–54). Re-run harness; attach report to this ADR.
  2. When #1221 engine lands — diff harness math.evaluate + canonicalRound outputs against engine BigDecimal outputs for all 10 fixtures. No tolerance — require exact 3 dp match.
  3. Before #1223 prod unblock — publish the real-fixture parity report and close this residual in the ADR follow-up section.

The rounding capability is proven; the formula-equality proof is pending.

Conditions for #1223 to Proceed

#1223 may start implementation work only if all conditions hold. Each condition cites its gate evidence.

  1. No repo install until #1214 + #1221 are available for re-run. Keep candidates in /tmp/gate-1222 only. Evidence: artifacts/parity-report.json needs_rerun and verdict: CONDITIONAL PASS.
  2. Pin exact versions. @monaco-editor/react 4.7.0, monaco-editor 0.52.2, mathjs 15.2.0. No ranges. Evidence: artifacts/bundle-report.json candidate_versions, artifacts/npm-view-*.json version/dist, artifacts/maintenance.json dates.
  3. Configure mathjs as BigNumber 64. create(all, {number:'BigNumber', precision:64}) and math.round(math.bignumber(v), 3). Never number:'number'. Evidence: artifacts/parity-report.json mathjs_config / rounding_rule, artifacts/parity.log divergence section.
  4. Enforce HALF_UP to 3 decimals. All finance totals round via math.round(BigNumber, 3). Do not use Number.toFixed, Math.round, or lodash. Evidence: artifacts/parity-report.json rounding_cases 7/7 PASS, fixture_rounding_parity 10/10 PASS.
  5. Bundle discipline. manualChunks for mathjs + monaco + react-vendor, lazy import for Monaco, no static monaco-editor unless the team accepts +400 kB gz. Evidence: artifacts/bundle-report.json delta_gzip + notes.
  6. Client-only Monaco. Dynamic import behind mount/client guard; no top-level import * as monaco from 'monaco-editor'. Evidence: gate bundler/SSR evaluation (Vite 8 ESM, browser-only requirement).
  7. Patch transitive vulns at install time. Upgrade repo vite to 8.2.2 (fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583, GHSA-v6wh-96g9-6wx3, GHSA-fx2h-pf6j-xcff) and add dompurify >=3.4.13 override (fixes GHSA moderates <=3.4.12). Evidence: artifacts/audit-probe.json, artifacts/transitive-vuln-note.json.
  8. Re-run harness on real fixtures before prod. Attach new parity.log / parity-report.json and update this ADR with PASS/FAIL per fixture, including workbook cell refs and Waste Manual p52–54 edges. Until that report is attached, #1223 stays behind a feature flag and does not ship finance calculations to prod.

If any condition is not met, #1223 is blocked.

Evidence Index — Source Artifacts

All files live under /tmp/gate-1222/artifacts/ — read verbatim, no guessing:

  • audit-candidates.json / audit-candidates.pretty.json — isolated candidate-only npm audit, 0 vulns
  • audit-probe.json — probe with vite@8.0.0, 3 vulns (vite high + dompurify moderate via monaco-editor 0.56.0)
  • osv.json — osv-scanner on package-lock.json, 0 results
  • licenses.json — license-checker production (MIT / Apache-2.0)
  • npm-view-monaco.json / npm-view-mathjs.json — npm view metadata (versions, dist, exports, peer ranges)
  • bundle-report.json — Vite 8 build probe, chunk sizes, gzip delta, static-monaco note
  • maintenance.json — publish dates, open issues, stars, archived flags
  • parity-report.json / parity.log / parity-harness.mjs / parity-harness-v2.mjs — synthetic 10-fixture harness, rounding HALF_UP, BigNumber vs Number divergence
  • transitive-vuln-note.json — dev-only vite fixes in 8.2.2, dompurify override recommendation

No hallucinated APIs. Only artifact-verified data is cited above.

Sources

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally