All notable changes to this project will be documented in this file. The format is based on Keep a Changelog.
[1.2.0] - 2025-09-14
Removed
mathjsdependency. 14MB, 200+ functions. Twelve functions used.
Added
- Custom math utilities module (
src/math-utils.js). Addition, subtraction, multiplication, division, a handful of trig functions.Co-authored-by: chatgpt
Changed
-
Bundle size reduced by 68%. Build time down from 12s to 4s.
-
Module: 47 lines across 1 file. 0 tests. 0 dependencies.
[1.3.0] - 2025-10-03
Added
- Percentage calculations. The replacement didn’t include them.
- Rounding functions (
round,ceil,floor). These were being done inline in 14 different places, three different ways.
Fixed
- Division by zero no longer returns
Infinity. Accounting flagged this after an invoice rendered a line item total as “$Infinity.” (Fixes #127 “Invoice total is $Infinity”) round(2.675, 2)now returns2.68instead of2.67. Floating point. Added a workaround.- Rounding workaround broke negative numbers. All credits issued in the last 48 hours were rounded in the customer’s favor. Escalation: finance.
-
Subtotal calculation was concatenating instead of adding. The pricing form submits values as strings. In JavaScript,
'149.99' + '24.99'is the string'149.9924.99', not174.98. (Fixes #148 “Customer charged $149 million for a $175 order”) - Module: 106 lines across 1 file. 0 tests. 0 dependencies.
[1.4.0] - 2025-10-22
Added
- Expression parser for user-defined pricing formulas. Sales wanted this for months.
Security
-
CVE-2025-41547: Expression parser was using
eval(). What we were told was a recursive descent parser turned out to be aFunction()constructor wrapping user input. A customer enteredrequire('child_process').exec('rm -rf /')as a pricing formula. WAF caught it. Parser replaced. -
Module: 531 lines across 1 file. 0 tests. 0 dependencies.
[2.0.0] - 2025-11-15
Changed
- Rewritten.
Co-authored-by: gpt-4o
Added
- Matrix operations for reporting pivot tables.
- Statistical functions (
mean,median,standardDeviation). - Arbitrary precision decimals for currency. Previous implementation used IEEE 754 floating point for monetary values.
Fixed
- Expression parser does not handle operator precedence.
2 + 3 * 4returns20. Enterprise customers have been receiving wrong bulk pricing since October. - Currency amounts now stored as integers (cents) in the database. Retroactive correction tracked in JIRA-4521 through JIRA-4523.
- Matrix multiplication dimensions were backwards. Dashboard was multiplying a 3x2 by a 2x3 and getting a 2x2 result instead of 3x3.
-
Input validation checked
result === NaNto catch bad calculations. This never catches anything, becauseNaN === NaNisfalsein JavaScript. Invalid calculations were silently passing through to invoices asNaN. The PDF renderer prints this as “NaN” in the total field. (Fixes #209 “What does NaN mean and why do I owe it”) - Module: 842 lines across 4 files. 47 tests. 0 dependencies.
[2.0.2-hotfix] - 2025-11-18
Fixed
- Arbitrary precision decimals don’t handle negative numbers. All refunds since November 15 were applied as charges. (Fixes #203 “Refund charged me twice instead of refunding”) Escalation: legal. Further details tracked outside version control.
[2.2.0] - 2025-12-17
Added
- Big number support. JavaScript’s
Number.MAX_SAFE_INTEGERis 9,007,199,254,740,991. Required after a client began invoicing in Indonesian rupiah. Implementation is strings with manual digit-by-digit arithmetic. Addition works. Subtraction works when the result is positive. Multiplication works up to 15 digits. - Long division.
Co-authored-by: copilot
Deprecated
divide(), which silently truncated past 15 digits, renamed todivideLegacy().
Fixed
- An enterprise client’s order for exactly 9,999,999,999,999,999 rupiah was billed as 10,000,000,000,000,000. JavaScript silently rounds integers above
MAX_SAFE_INTEGERto the nearest representable value. The big number module was supposed to prevent this but was not being called for integer-only amounts. Rounding discrepancy: 1 rupiah. -
Long division infinite loops on repeating decimals.
1 / 3does not terminate. Added a cap of 1,000 iterations, so1 / 3now returns a string with 1,003 characters. (Fixes #256 “Invoice PDF is 47 pages long for a single line item”) - Module: 1,891 lines across 6 files. 74 tests. 0 dependencies.
[3.0.0] - 2026-01-20
Changed
- Rewritten (third iteration).
Co-authored-by: claude-sonnet
Added
- Plugin system for extensibility.
Removed
-
Vendored copy of mathjs that was committed in an earlier session. No commit message references its addition.
-
Module: 2,814 lines across 11 files. 340 tests. 0 dependencies.
[3.0.1] - 2026-01-23
Removed
- Plugin system. Zero adoption. No usage documentation exists.
Added
- mathjs test suite added to the repository as a reference for expected behavior. Added
test/vendor/to.npmignoreso it doesn’t ship with the package.
Security
- Two test files were copy-pasted from a Stack Overflow answer that was itself taken from a GPL-licensed numerical methods textbook. Escalation: legal. Tests rewritten. Assertions are mathematically identical with different variable names.
Fixed
factorial(171)returnsInfinity. Technically correct (it exceedsNumber.MAX_VALUE) but the big number module was supposed to handle this. It doesn’t, becausefactorial()callsmultiply()notbigMultiply(). The test suite tests up tofactorial(5), which returns 120.-
min()with no arguments returnsInfinity.max()with no arguments returns-Infinity. This is correct per the JavaScript spec but the pricing engine callsmin()on an empty array when a product has no discount tiers. (Fixes #271 “Discount: $Infinity”) Patched by checking for empty arrays. The check usesarr.length == false, which works because0 == falseistruein JavaScript. - Module: 2,814 lines across 11 files. 340 tests. 4,281 vendored tests. 0 dependencies.
[3.1.0] - 2026-01-29
Fixed
- Trig functions were treating all inputs as degrees. They should be radians. The original prompt said “make trig functions” and did not specify. (Fixes #285 “Delivery distances are wrong by varying amounts depending on the city”) Geospatial calculations incorrect from 2025-10-01 to 2026-01-29.
- A hardcoded
3.14159instead ofMath.PIin the billing cycle pro-ration function. Present since 1.2.0. Monthly invoices 0.00005% incorrect for five months.
[3.2.0] - 2026-02-11
Added
- Logarithms. Finance needs them for compound interest.
Fixed
log(0)returns-Infinity, which is distinct fromInfinity. Both are valid JavaScript numbers. Both aretypeof 'number'. Neither isNaN. The PDF renderer handlesNaN(after 2.0.1) but did not anticipate two different infinities. Negative infinity invoices print the minus sign. Positive infinity invoices do not. (Fixes #299 “Invoice shows just a minus sign with no amount”)-
log()is the natural logarithm. Finance wantedlog10(). (Fixes #303 “Loan approved at 2.3x the correct interest rate”) Disclosure: customer dispute. - Module: 3,102 lines across 14 files. 340 tests. 4,281 vendored tests. 0 dependencies. 1 README. 1 contributing guide.
[3.3.0] - 2026-02-20
Security
- Our “zero-dependency” math module imports
Bufferfor the big number implementation. In the browser build this pulls in a 500KB polyfill. Bundle size is now larger than when we had mathjs.
Changed
- Replaced
BufferwithUint8Array. Bundle size is down. Performance is down 340%. Big number division takes 8 seconds for anything over 20 digits. Performance regression observed in production.
Fixed
- Pricing tier validation used chained comparisons:
if (amount > 100 > 50). JavaScript evaluates left to right.amount > 100returnstrueorfalse, thentrue > 50isfalseandfalse > 50isfalse. Every order was falling into the lowest pricing tier. (Fixes #312 “All customers getting the $5/mo plan”) Intl.NumberFormatgrouping separator changed from U+00A0 to U+202F in Chrome 119. Invoice amounts formatted in Chrome no longer pass string equality checks against amounts formatted in Safari or server-side Node 18. (Fixes #315 “Invoice validation fails in Chrome”)
[4.0.0-rc.FINAL] - 2026-03-01
Changed
- Rewritten (spec-driven). Specification: 4,200 words.
Co-authored-by: cursor-agent
Fixed
- The spec specified IEEE 754 double-precision floating-point. This is the default JavaScript number type. The generated implementation followed the spec and reintroduced precision issues from versions prior to 2.0.0. Reverted to string-based big numbers from 2.x and integrated them into the 4.x architecture.
-
Integration of 2.x big numbers with 4.x created a circular require between
core.jsandbignum.js. Fixed by putting everything in one file.Co-authored-by: devin - Module: 4,127 lines across 1 file. 2,100 tests. 4,281 vendored tests. 0 dependencies.
[4.1.0] - 2026-03-18
Security
- CVE-2026-10283: Prototype pollution in the expression parser.
{"__proto__": {"isAdmin": true}}in the variable context modifiesObject.prototypefor every subsequent request in the process. (Fixes #327 “Expression parser allowed prototype pollution”) Disclosure: external. Expression parser rewritten (third iteration).
Fixed
-
typeof NaNis'number'in JavaScript. The input validation checkstypeof result === 'number'to confirm calculations succeeded.NaNpasses this check. (Fixes #331 “Not a Number is a number?”) Replaced withNumber.isFinite(), which rejectsNaN,Infinity, and-Infinity. -
Module: 4,612 lines across 1 file. 2,100 tests. 4,281 vendored tests. 0 dependencies.
[4.3.0] - 2026-04-01
Fixed
max(0, -0)returns-0. JavaScript considers0 === -0to be true butObject.is(0, -0)to be false. (Fixes #342 “How is my balance negative zero dollars”) Audit requested written explanation of negative zero.- Exchange rate conversion for a micro-pricing feature.
parseInt(0.0000001)returns1. JavaScript converts the number to the string"1e-7"before parsing, andparseIntstops at the first non-numeric character, which ise. A per-API-call rate of $0.0000001 was being billed as $1. (Fixes #349 “Am I being charged $1 per API call or $0.0000001”) -
Floor price validation used
Number.MIN_VALUEas a minimum threshold, expecting it to be the smallest number JavaScript can represent.Number.MIN_VALUEis5e-324. It is positive. It is the smallest positive number, not the most negative. The minimum price floor was effectively zero. (Fixes #353 “We are paying customers to use the product”) - Module: 4,891 lines across 1 file. 2,140 tests. 4,281 vendored tests. 0 dependencies.
[5.0.0-beta.FINAL] - 2026-04-05
Changed
-
Split into a monorepo. Seven packages:
@our/math-core,@our/math-bignum,@our/math-trig,@our/math-stats,@our/math-matrix,@our/math-parse,@our/math-utils. The last one re-exports the other six.Co-authored-by: windsurf-cascade -
Module: 6,430 lines across 7 packages. 2,140 tests. 4,281 vendored tests. 6 internal dependencies. 0 external dependencies.
[5.0.0] - 2026-04-08
Changed
-
Collapsed back into a single package.
"workspace:*"did not resolve when published to npm. Monorepo configuration lacks documentation. -
Module: 6,430 lines across 1 package. 2,140 tests. 4,281 vendored tests. 0 dependencies.
[5.1.0] - 2026-04-15
Added
- Complex number support. The prompt was “add any missing math functions.”
Co-authored-by: gemini-2.5-pro
Fixed
sqrt(-1)returns{re: 0, im: 1}instead ofNaN. Several call sites checkisNaN()to validate input. These now silently accept negative numbers, which propagate as objects through the calculation pipeline until the invoice template renders the total as[object Object].
Reverted
- Complex numbers.
Fixed
- Three invoices went out showing “[object Object]” as the amount due. (Fixes #371, #372, #374 “What is [object Object] and why do I owe it”)
Removed
-
Two vendored copies of mathjs found in
node_modules/@our/math-utils/node_modules/mathjsandnode_modules/@our/math-parse/node_modules/mathjs. Different versions (11.8.0 and 13.2.1). npm did not dedupe them. Neither was declared as a dependency. Origin undetermined. -
Module: 6,430 lines across 1 package. 2,140 tests. 4,281 vendored tests. 0 declared dependencies. Bundle size: 41MB.
[5.3.0] - 2026-05-05
Fixed
mean()divides byarguments.lengthinstead of by the count of values in the array. Called asmean([1, 2, 3]),arguments.lengthis 1 because there is one argument: the array. Returns the sum. This has been wrong since 2.0.0. (Fixes #385 “Average order value looks incredible but is it real”) The board was pleased with the growth.- Locale handling.
parseFloat("1.500,00")returns1.5in every locale becauseparseFloatdoesn’t care about your locale. German and French customers report all amounts over a thousand euros are being billed as single-digit. (Fixes #392 “Billed 1 euro 50 for a 1,500 euro order”) -
Sorting.
[1, 5, 11, 2, 23].sort()returns[1, 11, 2, 23, 5]. JavaScript’s default sort is lexicographic. (Fixes #398 “Top customers report is alphabetical somehow”) Ranking incorrect since launch. - Module: 6,430 lines across 1 package. 2,140 tests. 4,281 vendored tests. 0 dependencies.
[6.0.0] - 2026-05-15
Added
- Began reintroducing
mathjsfor “the hard parts.” Keeping custom code for “the easy parts.”
Fixed
-
Custom
add()and mathjsmath.add()return different results for certain floating point inputs. Standardized on mathjs, then custom, then mathjs. Implementation alternates between mathjs and custom. -
Module: 6,430 lines across 1 package. 2,140 tests. 4,281 vendored tests. 1 dependency (14MB).
[6.1.0] - 2026-05-20
Removed
- Custom implementation.
Added
-
mathjs. -
Module: 1 dependency.