Skip to content

Release 3.5.0 - #1801

Merged
sequba merged 19 commits into
masterfrom
release/3.5.0
Oct 9, 2026
Merged

sequba merged 19 commits into
masterfrom
release/3.5.0

Conversation

@sequba

@sequba sequba commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Context

Release branch for HyperFormula 3.5.0. It contains develop and the release commit from npm run release -- code-freeze: the version bump, HT_RELEASE_DATE, the 3.5.0 CHANGELOG section, the release notes, and the demo URLs pointing at 3.5.x. The release-branch review added these fixes:

  • 55467f6: README demo link, plus the release script. README.md still linked the 3.4.x StackBlitz demo, because step 8 of code-freeze only rewrote demo URLs in docs/guide/ and docs/index.md. The link now points at 3.5.x. Step 8 now also scans README.md, and step 9 stages it so the rewrite is committed.
  • 00c0dd7: changelog formatting. MAXPOOL and MEDIANPOOL are formatted as code, like the other function names.
  • 60734d6: the check:licenses exclusion, plus the release script.
    • npm run check:licenses excluded hyperformula@3.1.1 from its own license check. Since 3.1.1 it has failed on the package itself, because GPL-3.0-only is not on the allow list.
    • license-checker only matches exclusions by exact name@version, so the exclusion now names hyperformula@3.5.0.
    • Step 3 of code-freeze now updates the exclusion together with the version bump. It skips the update when the exclusion is already current and warns when the script no longer has one.
  • fe16eed: accurate docs and changelog for Fix MAXPOOL and MEDIANPOOL throwing on non-tiling dimensions #1718 and Fix precision loss in variance, standard deviation and related statistics (HF-491) #1784.
    • MAXPOOL/MEDIANPOOL returned the following:

      Input 3.4.0 in a cell 3.4.0 via calculateFormula() 3.5.0
      window_size/stride not a positive integer throws throws #NUM!
      window larger than the range, or non-fitting dimensions throws throws #VALUE!
      stride > window_size #VALUE! result result; the windows skip the rows and columns between them
    • The catalogue entries and the JSDoc said #VALUE! for the first row, and none of the docs mentioned the third row. Both are now described in the catalogue, the JSDoc and the changelog: the Fixed entry names both errors, and a new Changed entry covers the larger stride.

    • STEYX gets a readable short description instead of the garbled one.

    • The changelog says COVARIANCE.P/COVARIANCE.S instead of COVARIANCE, and lists DEVSQ in the very-small-values fix. DEVSQ was fixed for very small values only; its results for large values are unchanged.

release-README.md documents both script changes, and the release notes mirror the changelog. npm run release -- publish 3.5.0 merges this branch into master and develop; this PR tracks CI and review in the meantime.

How did you test your changes?

  • CI on the release commit a704172 is green: Test, including the private suite and browser tests, Lint, Build on various envs, Build docs, and Security.
  • Vendored parser / drift is red on this PR for a reason outside it; see the comment below. It fails the same way on develop.
  • Locally: npm ci, npm run bundle-all, verify:typings, verify:publish-package, tsc --noEmit and npm run lint (0 errors, no new warnings) pass, and so do the smoke tests. npm run check:licenses now passes.
  • npm run docs:generate-function-docs renders the new descriptions, and getFunctionDetails() returns them.
  • The MAXPOOL/MEDIANPOOL table and the DEVSQ claim come from evaluating the same formulas on the published 3.4.0 package and on this branch.
  • I ran step 8's URL rewrite and the new step-3 block, both taken from release.sh, on copies of the old files, covering the dry run, the real run, a re-run and the warning path. bash -n script/release/release.sh passes.

Types of changes

  • Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore)
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues:

  1. Release 3.5.0

Checklist:

  • I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project.
  • I have signed the Contributor License Agreement.
  • My change is compliant with the OpenDocument standard.
  • My change is compatible with Microsoft Excel.
  • My change is compatible with Google Sheets.
  • I described my changes in the CHANGELOG.md file.
  • My changes require a documentation update.
  • My changes require a migration guide.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NNPv319TtBWAMhLYXNjoLS

sequba and others added 18 commits August 10, 2026 18:09
### Context

[HF-353](https://app.clickup.com/t/9015210959/HF-353) started from one
symptom: the official HyperFormula skill for Claude Code was documented
only on the [Set up your coding
agent](https://hyperformula.handsontable.com/docs/guide/setup-coding-agent.html)
page, and searching the docs for `skill` returned nothing.

The cause is general, not specific to that page. The docs search box
builds its match domain from a page's **title + `tags` frontmatter +
`h2`/`h3` heading titles** only
([`docs/.vuepress/plugins/search-box/match-query.js`](docs/.vuepress/plugins/search-box/match-query.js))
— body text is never indexed. Until now **no page in the repository used
`tags`**, so every page was findable only by the words it happened to
put in a heading. A reader who types the Excel term, the error value,
the method name or the abbreviation found nothing.

Changes:

- **Every guide page** (55 of them) now carries a `tags` list of the
words a reader really types: the Excel or Google Sheets term (`GSheets`,
`MS Excel`, `defined names`), the literal error value (`#REF!`,
`#DIV/0!`), the API method (`buildFromArray`, `setRowOrder`,
`registerFunctionPlugin`), the abbreviation (`CLA`, `GPL`, `CRUD`,
`SSR`, `IE11`, `UDF`), the third-party name (`ExcelJS`, `Pinia`,
`Chevrotain`, `Context7`) and the plain-language word for the problem
(`slow`, `memory`, `division by zero`, `quickstart`).
- **`docs/index.md`** and **`README.md`** — one paragraph at the end of
"Installation and usage" mentioning the skill and linking to the
coding-agent guide. The two files are hand-maintained mirrors of one
page, so the paragraph went into both.
- **`docs/guide/client-side-installation.md`** and
**`docs/guide/server-side-installation.md`** — both installation guides
now close with a short "Set up your coding agent" section pointing at
the same guide. These are the pages a reader actually lands on when
installing, and neither repeats the skill's setup commands.
- **`README.md`** (same page, drive-by) — its 22 documentation links,
and the one in `.github/pull_request_template.md`, still used the
pre-Cloudflare `hyperformula.handsontable.com/guide/<slug>.html` form.
The docs are built under `/docs/` and served from
`https://hyperformula.handsontable.com/docs/` (`docs/README.md`,
`docs/.vuepress/build.config.js`) — the form
`src/interpreter/functionMetadata/**` already uses — so all of them now
carry the `/docs/` prefix.
- **`DOCS_CONTENT_GUIDE.md`** — documents `tags` in "VuePress
conventions": what it is for, the 10-tag limit, why a tag ten pages
claim is worthless, and that it must be a YAML **list** (a plain string
makes `match-query.js` call `.join()` on a string and breaks the search
box site-wide).

The tags follow four rules:

1. **At most 10 per page**, fewer where fewer will do — the range is 2
to 10, 353 in total.
2. **Never a word the page's own title or headings already match** —
those match anyway, so such a tag buys nothing. This is why
`setup-coding-agent.md` lost `Cursor`, `Copilot` and `Claude Code` (all
in its own headings) and `LLM` (a substring of `llms.txt`) when it was
trimmed to the same limit.
3. **Never a generic word the page is not the best destination for**,
remembering that a multi-word tag is matched word by word, so `rules
file` would drag the page into results for `file` and `rules`.
4. **Only what the page truly answers** — nothing tags a feature it does
not document. `timezone`, for instance, is tagged nowhere, because no
page covers time zones.

`tags` has exactly one consumer in the repo (that search plugin), and
frontmatter is stripped from the `.md` companions and `llms-full.txt`,
so nothing outside the search index changes. Tags on
`built-in-functions.tmpl.md` reach the generated built-in functions
page, which `script/generate-builtin-functions-doc.ts` splices from that
template.

### How did you test your changes?

**The search behaviour.** `npm run docs:build` cannot run in the
authoring environment (no `node_modules`, no registry access), so this
was tested by **executing the shipped search code** — `match-query.js`
and `SearchBox.vue`'s `suggestions` / `suggestionsFromCategory` logic,
loaded through `vm` with `lodash/get` stubbed — against page objects
built from the real Markdown sources the way `@vuepress/core` 1.9.10
does (title from frontmatter or the first `H1`, headers from `h2`/`h3`
only, frontmatter from the YAML block).

Results over the whole tag set:

| check | result |
| --- | --- |
| tag queries run (every tag of every page) | 353 |
| tags that fail to surface their own page | **0** |
| tags returning more than 6 rows (limit is 10, unranked) | **0** |
| `skill` → the coding-agent page | `Guides: Set up your coding agent`
(was: no results) |

Spot checks land where they should, page-level or deep-linked: `#REF!` →
`Cell references > The #REF! error` and `Types of errors`; `CLA` →
`Contributing`; `GPL` → `License key`, `Licensing > GPLv3 license`;
`IE11` → `Supported browsers`; `buildFromArray` → `Basic usage`; `Pinia`
→ `Integration with Vue`; `modulo` → `Types of operators`; `trace
precedents` → `Dependency graph`; `division by zero` → `Types of
errors`.

The error values are quoted in YAML (`- "#REF!"`) on purpose: unquoted,
`#` starts a comment, and the unprefixed `REF!` would miss the `#REF!` a
reader actually types. The quoted form matches `ref`, `REF!` and `#REF!`
alike.

**The rest.**

- Every frontmatter block parses with a real YAML parser — `tags` reads
back as a list of strings on all 56 pages, and no page exceeds 10.
- The applier refuses to write a tag list that breaks an invariant (over
10, duplicates, a tag that is a substring of another on the same page,
an unquotable value), so none of those can slip in.
- `build-docs` (a real `npm run docs:build`) and the Cloudflare preview
deployment both passed on the commit carrying all 55 tagged pages, which
is the authoritative check that VuePress accepts this frontmatter.
- The new sections on the two installation guides keep `skill` pointed
at the coding-agent page (still a single row for that query); `coding
agent` now returns three rows — the guide itself plus the two new deep
links.
- Every rewritten README link was checked against the docs sources: all
21 distinct URLs resolve to an existing page, and the two anchors in use
(`#install-with-npm-or-yarn`, `#demo`) exist.
- README.md and docs/index.md were diffed under the link-form
normalisation and are line-for-line identical apart from the docs page's
frontmatter and its leading `<br>` spacing.
- The diff is Markdown and YAML only, so no lint, unit test or build
step is affected; each edited page keeps its own line-wrapping
convention, no trailing whitespace, no CRLF.

Worth a reviewer's eye:

1. `Codex` and `Windsurf` are tagged on the coding-agent page although
it never names them — they fall under its "Cursor, Copilot & other
agents" section, and pointing such a user at `llms-full.txt` is exactly
the advice they need.
2. Four pages share `SSR` and four share `breaking changes`; three share
the AI-integration vocabulary (`AI agents`, `LLM`, `tool calling`). Each
genuinely answers those queries, and the row counts stay well under the
limit, but it is the kind of thing to keep an eye on as more tags are
added.
3. `docs/index.md` is deliberately untagged: it has no `H1`, so VuePress
infers no title for it and its search rows would be labelled by path.

### Types of changes
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:
1. [HF-353](https://app.clickup.com/t/9015210959/HF-353)

### Checklist:
- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html)
and I confirm that my code follows the code style of this project.
- [ ] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [ ] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html)
standard.
- [ ] My change is compatible with Microsoft Excel.
- [ ] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [x] My changes require a documentation update.
- [ ] My changes require a migration guide.

Deliberately left out: no `CHANGELOG.md` / release-notes entry
(documentation-only change, and the 3.4.0 mention of the coding-agent
page was dropped from this PR on request). Four links of the old
`/guide/` form survive in `CHANGELOG.md` (3) and
`docs/guide/release-notes.md` (1) — those two files were off limits
here, so they are worth a separate one-line fix.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1751)

### Context

Follow-up to
[#1750](#1750), which
re-pointed the documentation links in `README.md` and the pull request
template but deliberately left `CHANGELOG.md` and
`docs/guide/release-notes.md` untouched. These are the four links that
were left behind.

All four used the pre-Cloudflare
`hyperformula.handsontable.com/guide/<slug>.html` form. The
documentation is built under `/docs/` and served from
`https://hyperformula.handsontable.com/docs/` (`docs/README.md`,
`docs/.vuepress/build.config.js`) — the form the rest of the repository
already uses — so each now carries the `/docs/` prefix:

| file | link |
| --- | --- |
| `CHANGELOG.md` (2.0.2 → Removed) | `supported-browsers.html` |
| `CHANGELOG.md` (2.0.0 header) | `release-notes.html` |
| `CHANGELOG.md` (2.0.0 header) | `migration-from-1.0-to-2.0.html` →
`migration-from-1.x-to-2.0.html` |
| `docs/guide/release-notes.md` (2.0.2 → Removed) |
`supported-browsers.html` |

The 2.0.0 migration guide link needed more than the prefix. `c27a9d5`
("Fix language files for Node+ESM (#1377)") renamed
`docs/guide/migration-from-1.0-to-2.0.md` to
`docs/guide/migration-from-1.x-to-2.0.md` and did not update the link,
so it pointed at a page that does not exist under either prefix. It now
uses the current slug.

`script/release/release.sh` generates the release notes from the
changelog section verbatim, so both files keep the same absolute URL and
stay mirrors of each other.

### How did you test your changes?

- Every rewritten URL was checked against the sources:
`docs/guide/supported-browsers.md`, `docs/guide/release-notes.md` and
`docs/guide/migration-from-1.x-to-2.0.md` all exist; the old
`migration-from-1.0-to-2.0` slug does not, which `git log
--diff-filter=R` confirms was a rename in `c27a9d5`.
- A repository-wide grep for the old
`hyperformula.handsontable.com/guide/` and `/api/` forms now returns
nothing in either file, and nothing anywhere else in the repository.
- Markdown-only change to two documentation files, so no lint, unit test
or build step is affected; both files keep their existing wrapping and
no other line is touched.

### Types of changes
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:
1. Follow-up to
[#1750](#1750)
([HF-353](https://app.clickup.com/t/9015210959/HF-353))

### Checklist:
- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html)
and I confirm that my code follows the code style of this project.
- [ ] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [ ] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html)
standard.
- [ ] My change is compatible with Microsoft Excel.
- [ ] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.

No changelog entry: this only corrects links inside the changelog and
the release notes, and the repository does not require an entry for a
documentation-only change. The edited entries belong to already-released
versions, so their wording is unchanged — only the URLs move.


---
_Generated by [Claude
Code](https://claude.ai/code/session_01AXFK3TQ32DUAP3LR2T9b5q)_

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit fac749e. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#1752)

### Context

`MOD` returned the result of JavaScript's `%` operator, which is the
**truncated** remainder and takes the sign of the **dividend**. Excel,
Google Sheets and the OpenDocument specification define `MOD` as the
**floored** remainder, which takes the sign of the **divisor**. The two
agree whenever the arguments share a sign and differ by exactly one
divisor when they do not, so every mixed-sign call was wrong:

| Formula | Excel / Google Sheets | Before | After |
|---|---|---|---|
| `=MOD(-3, 12)` | `9` | `-3` | `9` |
| `=MOD(5, -3)` | `-1` | `2` | `-1` |
| `=MOD(7, 3)` | `1` | `1` | `1` |
| `=MOD(-7, -3)` | `-1` | `-1` | `-1` |

Same-sign arguments and `=MOD(x, 0)` → `#DIV/0!` were already correct
and are unchanged.

**Why `%` plus a correction, and not a one-liner.** Both textbook
formulas are too lossy for a calculation engine, so `ModuloPlugin`
shifts the remainder given by `%` (which is exact for IEEE 754 doubles)
only when its sign disagrees with the divisor's. The rejected
alternatives, with the concrete failures now pinned by tests:

- `dividend - divisor * Math.floor(dividend / divisor)` rounds twice and
the multiplication scales the division's error back up — `MOD(1e308, 3)`
returns `0` instead of `2`, and a dividend of `Number.MAX_VALUE`
overflows to `-Infinity`.
- `((dividend % divisor) + divisor) % divisor` loses a remainder that is
negligible next to the divisor — `MOD(1e-20, 3)` returns `0` instead of
`1e-20` — and overflows to `NaN` once the intermediate sum exceeds
`Number.MAX_VALUE`.

The reasoning is recorded in the JSDoc on `flooredRemainder` so the next
reader does not "simplify" it back.

**Docs and metadata.** `MOD` is removed from the list of differences
(row and root-cause bullet) and from the deviations listed in
`DEV_DOCS.md`, which both asserted the old behaviour. The sign contract
moves out of the `divisor` parameter description and into
`shortDescription`, because `script/renderBuiltinFunctionsTable.ts`
renders only name, short description and syntax — parameter descriptions
never reach the generated guide page, so the contract was invisible
there before.

### How did you test your changes?

Tests live in the private suite, per `DEV_DOCS.md`:
**handsontable/hyperformula-tests#46**, on a branch of this same name,
so `fetch-tests.sh` pairs the two and this PR's CI runs them. That PR
takes `unit/interpreter/function-modulo.spec.ts` from 4 cases to 38 (one
assertion each) and corrects one stale golden value in
`unit/function-metadata-api.spec.ts` — see it for the full breakdown.

The old spec covered only same-sign arguments (`5,2`, `36,6`, `10.5,3`),
which is why this went unnoticed. The new cases add opposite signs in
both positions, exact division in all four sign combinations, zero and
fractional arguments, coercion, and the precision boundaries that
separate the correct implementation from the plausible-but-wrong ones.

Mutation-checked — the suite is not merely green, it discriminates:

| Implementation | Failures out of 38 |
|---|---|
| The fix | **0** |
| Old `dividend % divisor` | 13 |
| `dividend - divisor * Math.floor(dividend / divisor)` | 5 |
| `((dividend % divisor) + divisor) % divisor` | 3 |
| The fix without its exact-division guard | 4 |

Run locally in the exact CI arrangement (this branch, with the tests
repo checked out at `test/hyperformula-tests`):

```
Test Suites: 502 passed, 502 total
Tests:       3 skipped, 6214 passed, 6217 total
```

Other gates: `npx tsc --noEmit` clean; `eslint . --ext .js,.ts` 0
errors, and 0 new warnings in `src/` (the file's 2 warnings for a
missing class/method JSDoc are pre-existing and shared with every
sibling plugin). `npm run docs:generate-function-docs` regenerates
cleanly, and the resulting `MOD` row reads *"Returns the remainder when
one number is divided by another. The result has the same sign as
divisor. | MOD(dividend, divisor)"*.

### Types of changes
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [x] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:
1. Fixes #1747
2. HF-357
3. Tests: handsontable/hyperformula-tests#46

### Checklist:
- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html)
and I confirm that my code follows the code style of this project.
- [ ] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [x] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html)
standard.
- [x] My change is compatible with Microsoft Excel.
- [x] My change is compatible with Google Sheets.
- [x] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [x] My changes require a documentation update.
- [ ] My changes require a migration guide.

### Notes for the reviewer

Three judgement calls to confirm, none of them blocking:

1. **"Breaking change" left unticked.** Returned values do change for
anyone relying on mixed-sign `MOD`, and there is no config escape hatch
(only a custom-function reimplementation). I followed the closest
precedent instead: the empty-cell `MATCH`/`VLOOKUP` fix shipped in 3.4.0
as a plain `### Fixed` while also editing the list of differences.
Ticking the box would mean creating a migration guide, and no
`migration-from-3.x` guide exists yet. Happy to reclassify.
2. **A knock-on change at an unrepresentable divisor.** A divisor that
overflows to infinity is reachable through string coercion, and
`=MOD(-10, "1e400")` now returns `#NUM!` where it used to return `-10`.
That follows from the definition — for a positive divisor the floored
result must lie in `[0, divisor)`, so the only answer here is
unrepresentable — and `-10` was exactly the truncated-remainder bug.
Pinned by two tests rather than left to chance; say the word if you
would rather special-case non-finite divisors.
3. **`QUOTIENT` is deliberately untouched.** The identity `a = b *
QUOTIENT(a, b) + MOD(a, b)` no longer holds for mixed signs, because
`QUOTIENT` truncates. Excel has exactly the same inconsistency, so
changing it would break Excel parity.

Separately, while testing this I found a **pre-existing, unrelated
bug**, now filed as [HF-358](https://app.clickup.com/t/86cbb9ufr):
negative zero leaks through the array/range path for `MOD`, `ROUND`,
`INT` and `*` alike (`Interpreter.evaluateAst` applies `fixNegativeZero`
only when `isExtendedNumber(val)`, which is false for a
`SimpleRangeValue`). It predates this change and affects the same inputs
before and after it.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Mixed-sign `MOD` results change for any workbook that depended on the
old dividend-sign semantics; impact is limited to that function but can
alter live spreadsheet calculations.
> 
> **Overview**
> **`MOD`** no longer uses JavaScript’s `%` operator. It now returns the
**floored** remainder whose sign matches the **divisor**, matching
Excel, Google Sheets, and OpenDocument (e.g. `=MOD(-3, 12)` is **9**,
not **-3**). Same-sign inputs and `#DIV/0!` for a zero divisor are
unchanged.
> 
> `ModuloPlugin` routes non-zero divisors through a new
**`flooredRemainder`** helper that adjusts `%` only when the truncated
remainder’s sign disagrees with the divisor’s, avoiding less stable
one-liner formulas at extreme magnitudes. JSDoc on that helper records
why.
> 
> Docs follow the behaviour change: **CHANGELOG** entry, **`MOD`**
removed from the Excel/Sheets differences table and related prose in
**`DEV_DOCS.md`**, and function catalogue **`shortDescription`** updated
so the divisor-sign rule appears in generated built-in function docs.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9f423a6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
### Context

`MAXPOOL` and `MEDIANPOOL` computed the output array size without
checking that the pooling window actually tiles the input range. When
the range dimensions, reduced by the window size, were not whole
multiples of the stride, the kernel read past the last row of the input,
and an uncaught `TypeError` escaped the interpreter and the public API:

```ts
const hf = HyperFormula.buildEmpty({licenseKey: 'gpl-v3'})
const id = hf.getSheetId(hf.addSheet('S'))
hf.setSheetContent(id, [[3, 1, 2], [9, 7, 8], [5, 4, 6]])   // 3x3

hf.calculateFormula('=MAXPOOL(A1:C3, 2)', id)
// TypeError: Cannot read properties of undefined (reading '0')
hf.calculateFormula('=MEDIANPOOL(A1:C3, 2)', id)            // same
```

The same exception escaped `setCellContents()`. `maxpoolArraySize()`
already rejected such arguments, but `ArraySize.error()` is `(1, 1,
isRef: true)`, which `isScalar()` reports as scalar, so the formula was
evaluated as an ordinary scalar formula and the invalid arguments
reached the kernel anyway.

Related failure modes found while reproducing: a window larger than the
range, a stride that makes the last window overshoot, and a zero,
negative or fractional window size or stride all threw as well
(`TypeError` or `RangeError: Invalid array length`).

### Changes

- `MatrixPlugin.maxpool()` / `MatrixPlugin.medianpool()` now validate
the window size and the stride against the range dimensions and return
`#VALUE!` with a new `ErrorMessage.PoolDimensions` (`'Range dimensions
are not compatible with the window size and the stride.'`) instead of
running the kernel out of bounds. `#VALUE!` matches how `MMULT` reports
incompatible dimensions.
- The window size and the stride are declared as
`FunctionArgumentType.INTEGER` with `minValue: 1`, so a zero, negative
or fractional value returns `#NUM!` (`Value too small.` / `Value needs
to be an integer.`) instead of throwing.
- The new `isPoolWindowFittingInputArray()` predicate is shared with
`maxpoolArraySize()`, keeping the predicted array size and the runtime
validation in sync. It also carries the positive-integer requirement,
which fixes a second defect: `maxpoolArraySize()` predicted a
**negative** array size for a negative stride, because `(4 - 2) % -1 ===
0` passes a naive divisibility check. The cell then reported `#SPILL!`
instead of the `#NUM!` the function actually returns.
- The dimension constraint is documented in the `range` / `window_size`
/ `stride` entries of
`src/interpreter/functionMetadata/categories/matrix-functions.ts` — the
single source of truth from which `docs/guide/built-in-functions.md` is
generated — and in the JSDoc of both methods.

### Tests

In the private suite: **handsontable/hyperformula-tests#48**, on the
matching `claude/vigilant-gates-3dkoua` branch so `fetch-tests.sh` pairs
the two and this PR's CI runs against it.

New cases in `unit/interpreter/matrix-plugin.spec.ts` cover dimensions
that are not a whole multiple of the window size (in a cell and via
`calculateFormula()`), only one dimension divisible, a window larger
than the range, an overshooting stride, a zero/negative/fractional
window size and stride, and a window covering the whole range. The
existing `matrix maxpool`, `matrix maxpool, custom stride` and `matrix
medianpool on even/odd square` tests already guard the working cases.

Verified locally against the full private suite: **502 suites, 6230
tests passed, 3 skipped**. `npm run compile` passes.

### Types of changes

- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [x] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [x] Change to the documentation

### Related issues:

1. Fixes #...

### Checklist:

- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html)
and I confirm that my code follows the code style of this project.
- [ ] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [ ] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html)
standard. — `MAXPOOL` and `MEDIANPOOL` are HyperFormula-specific
functions, not covered by the standard.
- [ ] My change is compatible with Microsoft Excel. — not applicable,
these functions do not exist in Excel.
- [ ] My change is compatible with Google Sheets. — not applicable,
these functions do not exist in Google Sheets.
- [x] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [x] My changes require a documentation update.
- [ ] My changes require a migration guide.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Localized bug fix to HyperFormula-specific pooling functions with
stricter validation and clearer errors; no auth, data, or broad API
contract changes beyond replacing throws with spreadsheet errors.
> 
> **Overview**
> **MAXPOOL** and **MEDIANPOOL** no longer throw uncaught `TypeError`
(or related runtime errors) when the range does not tile cleanly with
the window and stride. They now validate arguments up front and return
**#VALUE!** with `ErrorMessage.PoolDimensions`, consistent with how
incompatible dimensions are handled elsewhere (e.g. **MMULT**).
> 
> Shared helpers `isPoolWindowFittingInputArray()` and
`poolFunctionArgumentsError()` enforce positive integer window size and
stride, window fit inside the range, and divisibility of `(dimension -
window_size)` by stride. The same predicate is used in
`maxpoolArraySize()` so array-size prediction matches runtime behavior
(including cases that previously mis-predicted size for invalid
strides).
> 
> `window_size` and optional `stride` are typed as **INTEGER** with
`minValue: 1` instead of generic numbers, so zero, negative, or
fractional values surface as **#NUM!** via argument coercion rather than
reaching the kernel. Function metadata and the changelog document the
tiling rules and the fix.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
60606ca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
https://app.clickup.com/t/9015210959/SU-637

Goal: Improve docs-assistant answer depth on HyperFormula named ranges /
structured references
Change: Named-columns section: no Excel structured refs, why
SUM(Name1:Name5) fails, $A:$A workaround.

### Context

Adds the missing named-column recipe to the named-expressions guide so
docs-assistant can retrieve it. Cross-links the existing range-restraint
on cell-references.

### How did you test your changes?

Read-through against naming rules (`Name1` is A1) and range restraints
(`:` cannot take named endpoints). Docs-only; no engine change.

### Types of changes

- [x] Change to the documentation

### Related issues:
1. https://app.clickup.com/t/9015210959/SU-637

### Checklist:

- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html)
and I confirm that my code follows the code style of this project.
- [ ] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [x] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html)
standard.
- [x] My change is compatible with Microsoft Excel.
- [x] My change is compatible with Google Sheets.
- [ ] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.

Docs-only; no CHANGELOG 


Made with [Cursor](https://cursor.com) and Ola :)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
### Context

HyperFormula 3.4.0 shipped `VSTACK` and `HSTACK` with English
placeholder names in language packs where Microsoft Excel uses localized
names. This PR moves the previously reviewed localization fix out of the
unrelated TAKE PR and into a dedicated change.

This extracts the VSTACK/HSTACK portion of commit `b902d53bc`,
originally authored by Kuba Sekowski.

### Changes

| Language | VSTACK | HSTACK |
|---|---|---|
| Czech | `SROVNAT.SVISLE` | `SROVNAT.VODOROVNĚ` |
| Danish | `VSTAK` | `HSTAK` |
| German | `VSTAPELN` | `HSTAPELN` |
| Spanish | `APILARV` | `APILARH` |
| Finnish | `VPINO` | `HPINO` |
| French | `ASSEMB.V` | `ASSEMB.H` |
| Hungarian | `FÜGG.HALMOZÁS` | `VÍZSZ.HALMOZÁS` |
| Italian | `STACK.VERT` | `STACK.ORIZ` |
| Norwegian | `VSTAKK` | `HSTAKK` |
| Dutch | `VERT.STAPELEN` | `HOR.STAPELEN` |
| Polish | `STOS.PION` | `STOS.POZ` |
| Portuguese | `JUNTARV` | `JUNTARH` |
| Russian | `ВСТОЛБИК` | `ГСТОЛБИК` |
| Turkish | `DÜŞEYYIĞ` | `YATAYYIĞ` |

Indonesian and Swedish remain in English, matching Excel. The PR also
adds source-verification guidance to `DEV_DOCS.md` and an Unreleased
changelog entry.

### Source verification

All 14 localized pairs and the unchanged Indonesian and Swedish pairs
were checked on 2026-08-25 against Microsoft Support's current
individual VSTACK/HSTACK pages, using each page title and displayed
formula syntax. This caught one mismatch in the original table: Danish
is
[`VSTAK`](https://support.microsoft.com/da-dk/excel/functions/vstack-function)
/
[`HSTAK`](https://support.microsoft.com/da-dk/excel/functions/hstack-function).

Microsoft's Danish alphabetical function index currently conflicts with
those individual pages, so `DEV_DOCS.md` now requires checking the
linked individual page rather than copying a name from the index alone.

### Migration note

This is a breaking change for formulas created with HyperFormula 3.4.0
that use English `VSTACK` or `HSTACK` while one of the affected language
packs is active. Those formulas must switch to the names listed above.

### Validation

Companion tests: handsontable/hyperformula-tests#44

- 34/34 focused i18n tests passed.
- Full browser suite passed: 12,388 assertions in Chrome and Firefox.
- TypeScript compilation passed.
- Targeted lint completed with zero errors.
- `git diff --check` passed.

### Types of changes

- [x] Breaking change
- [ ] New feature or improvement
- [x] Bug fix
- [x] Additional language file or translation change
- [x] Documentation change

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Breaking change to parsed function names in 14 locales; no calculation
logic changes, but existing localized formulas using English
`VSTACK`/`HSTACK` will stop resolving until renamed.
> 
> **Overview**
> Replaces English placeholder names for **`VSTACK`** and **`HSTACK`**
in **14 built-in language packs** (Czech, Danish, German, Spanish,
Finnish, French, Hungarian, Italian, Norwegian, Dutch, Polish,
Portuguese, Russian, Turkish) with the localized identifiers Microsoft
Excel uses—for example German `VSTAPELN`/`HSTAPELN` and French
`ASSEMB.V`/`ASSEMB.H`. Locales where Excel keeps English (e.g. `enGB`,
Indonesian, Swedish) are unchanged.
> 
> **`DEV_DOCS.md`** now requires Excel-verified names only (no inferred
translations), documents Microsoft’s per-locale alphabetical function
list for newer functions, and says to confirm names on individual
function pages when the index disagrees. **`CHANGELOG.md`** records the
fix under Unreleased.
> 
> **Migration:** With an affected language pack active, formulas that
still call `VSTACK`/`HSTACK` in English must use the new localized
names; this is treated as a breaking change for 3.4.0-era workbooks.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e2c90f1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
### Context

`AVERAGEIF` returned a `#DIV/0!` error when matching values produced a
valid average of `0`.

This happened because the result used a logical OR fallback, which
treated `0` as if no average had been
calculated. This change uses nullish coalescing so that only an absent
result produces `#DIV/0!`, while a
  valid zero is returned normally.

  ### How did you test your changes?

Added a regression test where the matching values are `-1` and `1`,
producing an average of `0`.

The test was verified to fail with `#DIV/0!` before the fix and pass
with `0` after the fix.

  I also ran:

  - The focused `AVERAGEIF` test suite: all 14 tests passed.
- `npm run bundle:cjs`: TypeScript and the CommonJS build completed
successfully.
  - Targeted linting: no errors.

  ### Types of changes

- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as
  expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
  - [x] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
  - [x] Change to the documentation

  ### Related issues:

  None.

  ### Checklist:

- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://
hyperformula.handsontable.com/guide/contributing.html) and I confirm
that my code follows the code style of
  this project.
- [ ] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [x] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/
  os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard.
  - [x] My change is compatible with Microsoft Excel.
  - [x] My change is compatible with Google Sheets.
- [x] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/
  CHANGELOG.md) file.
  - [ ] My changes require a documentation update.
  - [ ] My changes require a migration guide.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single-operator change in AVERAGEIF result handling with no impact on
auth, data, or broader aggregation APIs.
> 
> **Overview**
> **`AVERAGEIF` now returns `0` when the conditional average is
legitimately zero**, instead of incorrectly surfacing `#DIV/0!`.
> 
> The bug came from using logical OR (`||`) after `averageValue()`: a
computed average of `0` was treated like a missing result.
**`ConditionalAggregationPlugin`** switches that fallback to nullish
coalescing (`??`), so only `undefined` (no matching numeric cells / zero
count) still maps to `#DIV/0!`. **CHANGELOG** documents the fix under
Unreleased.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2eb85e9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
## What & why

The docs stated a **hardcoded** count of built-in functions
("~400"/"400+") and languages ("17"/"18") that drifts from reality. The
function count already auto-derived via `{{ $page.functionsCount }}`;
this adds the parallel `{{ $page.languagesCount }}` and swaps the
remaining hardcoded counts to the interpolated variables.

## How

- `docs/.vuepress/config.js`: derive `languagesCount` once at config
load from the i18n export barrel `src/i18n/languages/index.ts`
(whitespace-tolerant regex + fail-loud guard so a barrel reformat can
never silently publish "0 languages"); inject `$page.languagesCount`.
- Docs: `~400`/`400+` → `{{ $page.functionsCount }}` (index, ai-sdk,
mcp-server, langchain); `17`/`18` → `{{ $page.languagesCount }}` (index,
built-in-functions, i18n-features, localizing-functions).
- `README.md` (not a VuePress page, so no interpolation possible):
manual `over 400` + `18`, with the language count now asserted against
the same i18n barrel at config load (`3b4fda30f`) so it fails the docs
build instead of rotting silently.
- `docs/.vuepress/plugins/md-companions/index.js`: register
`languagesCount` with the companion `{{ $page.* }}` resolver
(`3ef3fdddc`). The resolver arrived with #1703 and substitutes an
allowlist of injected keys, so without this the built `.md` companions
and `llms-full.txt` shipped the raw mustache while the HTML rendered the
number.

## Verification

`docs:build` renders the real counts (functions / languages), including
inside markdown link text, with **no un-rendered `{{ }}`** in the built
output. Docs-only + docs-build-config → no CHANGELOG per DEV_DOCS DoD.

## Known nuance

`README.md` counts stay hand-edited — it is rendered by GitHub and npm,
neither of which runs VuePress, so `{{ $page.* }}` would publish
literally. The language count is therefore guarded rather than
interpolated: the docs build fails if README disagrees with the barrel,
or if the `<n> built-in languages` phrase disappears. Verified both
failure modes. The function count needs no guard — "over 400" is chosen
so the line does not restale as functions grow. The
`localizing-functions.md` language *table* rows remain hand-maintained.

The guard couples the docs build to `README.md`, which is a deliberate
trade: the alternative was generating README as a build product, and it
is a file people edit directly.

ClickUp task: https://app.clickup.com/t/9015210959/HF-282

**Update (28.08 rebase):** `develop` replaced
`docs/guide/built-in-functions.md` with a generated page (template
`docs/guide/built-in-functions.tmpl.md` + generator, HF-249/#1692). This
PR's one-line change to that page now lives in the **template** — the
generator copies prose verbatim, and `npm run docs:build` renders `{{
$page.languagesCount }}` correctly in the generated output (verified:
zero unrendered mustaches in `dist/`). Commit SHAs in this description
refer to the rebased branch.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Docs and VuePress build configuration only; no runtime library or API
behavior changes.
> 
> **Overview**
> Stops hardcoded **function** and **language** totals in docs and the
root README from drifting when the catalogue or i18n packs change.
> 
> **VuePress** now derives `languagesCount` at config load from
`src/i18n/languages` (with a barrel re-export check) and injects `{{
$page.languagesCount }}` alongside the existing function count. The
function total is computed once via `getAvailableFunctions()` on a
default-config engine—the same approach as
`script/generate-builtin-functions-doc.ts`—instead of
`getRegisteredFunctionNames`, so the headline number matches the
generated built-in-functions table. Guide pages, index, and integration
previews swap `~400` / `400+` and `17` / `18` for `{{
$page.functionsCount }}` and `{{ $page.languagesCount }}`; the
**md-companions** allowlist resolves `languagesCount` in shipped `.md` /
`llms-full.txt`.
> 
> **README.md** (GitHub/npm, no VuePress) is updated manually to **over
400** functions and **18** languages; the docs build **fails** if the
i18n features bullet’s language number disagrees with the derived count.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ea8e6a4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
## Summary

The first-party docs MCP server is live at
`https://docs-assistant.handsontable.com/mcp` — semantic search over the
full HyperFormula + Handsontable knowledge base (docs guides, API
reference, code recipes, release notes, blog posts, and GitHub issues),
always current with the latest release. This PR makes it the recommended
MCP route in the agent-facing docs:

- **Set up your coding agent** guide: the "Live docs via MCP" section
now leads with the first-party server (GitMCP and Context7 stay as
alternatives); added to the page tags and the Resources list.
- **Wizard**: the Cursor path now hands out a `.cursor/mcp.json` config
for the server (Cursor supports MCP natively; the rules-file corpus
pointer stays as the noted alternative), and the Other/API path mentions
the MCP URL.
- **README**: one sentence pointing MCP-capable agents at the server.

The endpoint was verified live before this PR: initialize, `tools/list`,
and a real `tools/call` all pass, serving the current knowledge-base
pair (HT 18.1.0 / HF 3.4.0).

## Test plan

- Docs-only change: `docs/guide/setup-coding-agent.md` renders with the
existing anchors unchanged (the `#live-docs-via-mcp-any-agent` sidebar
link still resolves)
- Wizard JSON snippet is valid `.cursor/mcp.json` content

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only updates to README, docs pages, and the coding-agent
wizard; no runtime or library code changes.
> 
> **Overview**
> This PR updates agent-facing documentation to **recommend the
first-party docs MCP server** at
`https://docs-assistant.handsontable.com/mcp` for live, semantic search
over HyperFormula and Handsontable docs.
> 
> In **Set up your coding agent**, the “Live docs via MCP” section now
leads with that server (GitMCP and Context7 remain alternatives), and
the Resources list adds a link to the Docs MCP Server guide. The
**CodingAgentWizard** Cursor path now copies a `.cursor/mcp.json`
snippet for the server, with the `llms-full.txt` rules-file approach
noted as a fallback; the Other/API option also mentions the MCP URL.
**README** and **docs index** gain a short paragraph pointing
MCP-capable agents at the same server.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b0a6520. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…, API guards (#1728)

### What this is

HF-307, feature packages and add-ons. A proprietary license key can now
grant a *subset* of the library instead of only answering yes/no. The
key carries a flat list of capability tokens, and the engine grants the
union of what those tokens name.

This started as a stack of four PRs and is now one. #1729, #1730 and
#1743 were merged into this branch; #1731, #1736, #1737, #1740 and #1741
were closed once their content landed here. Paired tests:
handsontable/hyperformula-tests#30.

Twenty-four files are new; 22 of those live under `src/license/`.

> The earlier revisions of this description, written while the stack
still existed, are in this description's edit history.

### Two gates

- **Gate A** — is the key valid? Classic keys are validated and messaged
as before, except that the build release date and the expiry date in the
console message are now read in UTC (see below); entitlement keys are
validated too, with their own console messages. A key that is missing,
invalid, or expired in a way that blocks evaluation (a classic key, or a
trial past its grace period) blocks the library:
- every function except `VERSION` and `OFFSET` evaluates to `#LIC!`, as
before;
- new: every gated public-API method throws
`LicenseCapabilityMissingError` naming the key's state (`License key is
missing. Feature crud is not available.`), and so does building an
engine with named expressions. Before, such a key stopped only function
calls: operator formulas, plain data and the whole API kept working.
Code that builds an engine without a valid key now gets this error from
its first gated call. The changelog lists it under Changed.
- **Gate B** — what does the key grant? New. Functions and public-API
areas, resolved on two axes (`functions`, `features`), each either a set
or `'all'`.

One invariant binds them: **only a key that lets the build evaluate
restricts anything through gate B**: a valid key, or an expired
subscription or perpetual key, which keeps its own grants. A missing,
invalid or blocking expired key resolves to an entitlement that
restricts nothing, and gate A alone stops it. Its errors therefore
always name the key's state (`License key is invalid.`), never a missing
grant (`… is not included in your license`), and
`getAvailableFunctions()` still describes the whole catalog under it.

### What a restricted key does

- A function outside the grant evaluates to `#LIC!` rather than
disappearing, in its own cell: an array function the license stops
reserves no spill range, so it never shows `#SPILL!` instead.
- A gated public-API method throws `LicenseCapabilityMissingError`. Its
`feature` property names the area the call needed, as a value of
`FeatureId`, which the package now exports.
- The matching `isItPossibleTo*` predicates (`isItPossibleToAddRows()`
and the rest) answer `false` for a call that would throw it, and so do
`isThereSomethingToUndo()` and `isThereSomethingToRedo()` without
`undo_redo`.
- `getAvailableFunctions()` and `getFunctionDetails()` describe only
what the instance can actually evaluate. `getRegisteredFunctionNames()`
still lists every registered function.
- Valid classic 25-character keys, `gpl-v3`,
`internal-use-in-handsontable` and
`hftrial-0168e-1f2b7-47158-70b05-0842f` are unaffected: they resolve to
an unrestricted entitlement and never consult the capability table. An
invalid or expired classic key gets gate A's behavior above.

### Capability tokens

Function grants use the packaging vocabulary: `fun:all`,
`fun:<family>.<A|B|C>` and per-function `fun:<NAME>`. The callable
operator forms (`HF.ADD` and friends) sit under `fun:operator.A`; infix
operators work under any key. Token names are matched
case-insensitively.

Gated API areas use `feat:*` tokens, one area each. A key is granted
**exactly the areas it names**, and `feat:all` is how it names all of
them; a key naming none is granted none. The key generator
(`license-key` 5.1.1) cannot write `fun:*` or `feat:*` tokens yet, so a
key it issues today grants no gated function and no gated API area.

An earlier revision read the absence of a `feat:*` token as "this key
does not talk about features" and granted all five areas; that rule was
removed deliberately.

The engine knows no packages and no add-ons. A key may carry a word like
`spreadsheet` or `functions_1`; it is read as any other word this
version does not know: it grants nothing, nothing reports it, and it
never subtracts anything.

### The key reader

`src/license/handsontable-license-key-parser/` is a copy of the reader
upstream publishes for products (`license-key`'s
`vendor/entitlement-key-reader/`, tag 5.1.1). Twelve of its thirteen
files are byte-identical; the thirteenth differs by one cast, explained
below. The engine calls its `readEntitlementLicense`, as upstream's
README prescribes: the reader verifies the key, picks the `hyperformula`
entry, places it in its lifecycle window and reads its flags.
`licenseResolution.ts` keeps what the guide leaves to the product: the
meaning of the capability tokens, the console messages, and which
lifecycle states block evaluation. A non-trial key past its grace
period, or past the build its maintenance covers, reports EXPIRED and
prints a console error but keeps evaluating with its own grants. A trial
past its grace still gives `#LIC!`. Classic 25-character keys are read
as before, with one fix: the build release date they are compared with
is parsed in UTC, and the expiry date in their console message is
printed in UTC. Previously, east of UTC, a classic key that expired the
day before the build was released was still accepted, and west of UTC,
the console printed an expiry date one day too early. The changelog has
a Fixed entry for it.

Since 5.x, keys are in format version 2: the payload carries a digest of
the prose, so a key whose prose was edited or removed (the bare `[...]`
block), or that has text after its block, is invalid (M7). Whitespace
and line breaks saved as text (`\n`) are ignored anywhere in the key,
inside the block too. Following that, the license key guide tells users
to pass the whole key as issued. The spec's state table has the matching
rows (S31 changed, S39 added).

Console messages for entitlement keys are the specification's text, per
lifecycle state, the same table Handsontable prints
(`handsontable/src/helpers/mixed.ts`): the date exactly as the key
carries it, a warning while the key works and an error once it has run
out (the grace period included). They print every time a key is
resolved: unlike classic keys, which keep their once-per-page flag,
entitlement keys keep no record of what they already printed, so one key
can print the same message more than once on a page. The cast in
`extractKeyData.ts` stays.

Following the reader changed three things:

- A key that grants other products but not HyperFormula is INVALID and
restricts nothing. An earlier revision had it VALID with nothing
granted.
- A malformed date in another product's entry invalidates the whole key
again, as upstream does.
- Only `no-console-warns` silences the console. `silent-console` and
`silent` are unknown flags; the generator emits neither.

`upstream.json` pins the tag. `npm run check:license-key-parser-drift`
compares every file git tracks in the directory against the pinned
commit and runs in CI (`vendored-parser.yml`; currently red: the
`LICENSE_KEY_REPO_TOKEN` secret exists but cannot read the upstream
repository). One declared divergence, a single cast for TypeScript 4.0,
is recorded there with its expiry.

### Also in this PR

- **Moving or pasting a formula with an undefined name to another
sheet** no longer adds that name as an empty global named expression.
Before, the formula's `#NAME?` turned into an empty value, and the
workbook serialized a named expression nobody added, which a key without
`named_expressions` then could not build again. The bug predates this
PR; the license gate turned it into a hard failure. Only a name the
source sheet defines locally is copied to the global scope now
(`Operations.updateNamedExpressionsForTargetAddress`).
- **One rule for stopped function calls.** `FunctionCallLicenseGate`
decides which calls the license stops and with which `#LIC!` error. The
interpreter and the array size predictor both use it, which is what
keeps a stopped array function to one cell.
- **API reference examples.** Every `@example` block in `HyperFormula`
and its events, and the API reference landing page, passes `licenseKey:
'gpl-v3'`. Four examples that threw for their own reasons
(`moveColumns`, `getNamedExpressionValue`, `changeNamedExpression`,
`getAllNamedExpressionsSerialized`) are fixed.

### `VERSION()`

`VERSION()` returns only the version (`HyperFormula v3.4.0`), with no
license status, under every key. The function's catalog description and
the changelog say so.

### Docs

- Every engine built in a `docs/guide` example passes `licenseKey:
'gpl-v3'`: without a key, the gated methods those examples call now
throw.
- `DEV_DOCS.md` describes `src/license/` and the vendored reader, and
the new-function checklist has a step for `functionCapabilities.ts`: a
built-in missing from it is not gated at all.
- The changelog has a Changed entry for what a blocking key does to the
gated API. It is not marked as breaking, for the reason under "Two
gates".

### Not built here, on purpose

- The shared conformance fixtures both products would run in CI. Needs
the other side to exist.

### Review

Reviewed on 2026-09-22, 2026-09-23, 2026-09-24, 2026-09-28 and
2026-09-30, with the 2026-09-29 rulings in the thread. The
`feat:import_export` token is gone. Merge order: this PR before
hyperformula-tests#30.

Open:
- The drift-check workflow is red: its secret cannot read the upstream
repository.
- The key generator cannot write `fun:*` or `feat:*` tokens yet, so no
key issued today grants a gated function or API area.
- Spec rows S32–S35 and S38 have no tests yet.
- Two places where the engine and the spec disagree: a custom function
evaluates to `#LIC!` under a blocking key, and `getAvailableFunctions()`
lists the whole catalog under a blocking key.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Changes core licensing behavior for formula evaluation and most
mutating public API paths; incorrect gating or key parsing could block
paying customers or expose paid features.
> 
> **Overview**
> Adds **feature-package licensing**: proprietary keys can grant subsets
of functions (`fun:*` tokens) and API areas (`feat:*` tokens).
Restricted functions evaluate to **`#LIC!`**; gated API methods throw
**`LicenseCapabilityMissingError`**, with matching **`isItPossibleTo*`**
/ undo-redo predicates returning **`false`**.
**`getAvailableFunctions()`** / **`getFunctionDetails()`** now list only
what the instance’s key allows.
> 
> **Gate A** still blocks evaluation for missing/invalid keys and
blocking expirations (classic or trial past grace): almost all functions
become **`#LIC!`** (except **`VERSION`** / **`OFFSET`**), and gated
API—including building with **named expressions**—throws instead of
silently allowing edits. **`VERSION()`** no longer embeds license
status.
> 
> Integrates a **vendored** `handsontable/license-key` entitlement
reader (pinned tag, drift CI, one TS 4.0 cast shim),
**`licenseResolution`**, and capability tables in
**`functionCapabilities.ts`** / **`featureCapabilities.ts`**. Classic
25-char keys get a **UTC** fix for release vs expiry comparison and
console dates.
> 
> Also fixes **move/paste** of formulas referencing undefined names on
another sheet (no spurious global named expression), updates
docs/examples with **`licenseKey: 'gpl-v3'`**, and excludes the vendored
tree from ESLint/Codecov.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f9819f7. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…tics (HF-491) (#1784)

### Context

The variance functions compute `sum(x^2) - sum(x)^2 / n` in one pass.
When the mean is large relative to the spread (typical of measurement
data), the two terms are nearly equal and cancel, so every significant
digit is lost:

| Formula | Before | Exact (stored doubles) | After |
|---|---|---|---|
| `=STDEV.S({10000000.000,10000000.001,10000000.002})` | `#NUM!` |
0.0010000001639127731 | 0.0010000001639127731 |
| `=VAR.S(` same `)` | `-0.03125` (negative, no error) |
1.0000003278255731e-06 | 1.0000003278255731e-06 |
| `=STDEV.S({100000000000.003,100000000000.004,100000000000.005})` | `0`
| 0.0009994603901554338 | 0.0009994603901554338 |

`smartRounding: false` and `precisionEpsilon: 0` do not change the old
results.

`DEVSQ`, `COVARIANCE.P`, `COVARIANCE.S`, `SLOPE`, `STEYX` and the
database functions `DSTDEV`, `DSTDEVP`, `DVAR`, `DVARP` compute the mean
first and lose digits in the same situation when the values use all 15
significant digits. For example, for three values near `100000000000`,
`STEYX` returned `0.0187` instead of `0.00623` and `SLOPE` returned
`1000.37` instead of `1000.53`.

Affected:

- `VAR.S`, `VAR.P`, `STDEV.S`, `STDEV.P`, `VARA`, `VARPA`, `STDEVA`,
`STDEVPA`, their legacy names (`VAR`, `VARP`, `VARS`, `STDEV`, `STDEVP`,
`STDEVS`), and `SUBTOTAL` modes 7, 8, 10 and 11 (and 107, 108, 110,
111), all backed by `MomentsAggregate`
- `DEVSQ`, `COVARIANCE.P`, `COVARIANCE.S` (and `COVAR`, `COVARIANCEP`,
`COVARIANCES`), `SLOPE`, `STEYX`, `DSTDEV`, `DSTDEVP`, `DVAR`, `DVARP`

### The change

**Double-double helpers** (`src/interpreter/doubleDouble.ts`): TwoSum
(in the Fast2Sum form, taking the error from the larger addend, so it
cannot overflow while the sum is finite) and Dekker's TwoProduct with
Veltkamp's split (no FMA needed), plus addition, subtraction,
multiplication and division of double-doubles, and rounding to a double.
Near the top of the double range they rescale by exact powers of two:
`twoProduct` scales the larger factor by 2^-53 when a factor exceeds
2^996 or the product exceeds 2^1023, so its error term is exact for
every finite product above 2^-969, and the divisions scale dividends
above 2^1000 by 2^-64.

**`MomentsAggregate`** (`src/interpreter/plugin/MomentsAggregate.ts`)
stays composable, so the per-range cache keeps working. Besides the
count, it keeps the sums of deviations `S1` and of squared deviations
`S2` from a `shift` (the first value added), stored as double-double,
and rebases them when two aggregates compose. The deviations are stored
multiplied by `2^-exponent`, an exact power of two kept in the
aggregate, so the shifted sums cannot overflow: the exponent is 0 until
the scaled sum of squares exceeds 2^960 (deviations of about 1e144 and
above), and two aggregates compose at the larger exponent, raised
further when the difference of their shifts needs it. Adding one value,
the common case when a range is folded cell by cell, takes a fast path.
The sum of squared deviations `S2 - S1^2 / n` is evaluated in
double-double as `S2 - S1 * (S1 / n)`, so the `S1^2` term cannot
overflow, the variance is rounded once, and it is then multiplied by
`2^(2 * exponent)`. The variance is `#NUM!` only when it exceeds the
largest double, and it does not depend on the order of the arguments:
`=VAR.S(0, 1e154, 1e154)` and `=VAR.S(1e154, 1e154, 0)` both return
3.33e307 (develop returns `#NUM!` for both), and `=VAR.S(-1.2e154, 0,
1.2e154)` returns 1.44e308 although the sum of squared deviations
exceeds the largest double. The exponent can also go negative (down to
-600) when tiny differences follow equal values, and the standard
deviation is taken at the scale of the sums before scaling back, so
`STDEV.*` is finite when only the variance overflows and non-zero for
spreads below about 1e-154 (`=STDEV.S(1e-170, 2e-170)` returns
7.07e-171).

**Deviation sums** (`src/interpreter/deviationSums.ts`): `DEVSQ`,
`COVARIANCE.*`, `SLOPE` and `STEYX` compute the mean in double-double,
then the deviations and the sums of their products in double-double, and
round once. Each array is measured at its own power-of-two scale and the
rounded result is scaled back: an array whose largest magnitude is below
2^-400 is scaled up to it (exact), so squared deviations cannot
underflow and the sum of squared x deviations is 0 only when all the x
values are equal; an array above 2^400 is scaled down to it only when
the unscaled sums overflow, so on ordinary data the results are
bit-identical to the unscaled computation. `DEVSQ` is only scaled up,
because when its unscaled sum overflows, its result does too. `SLOPE`
and `STEYX` share `regressionSums` and the check for equal x values.
`STEYX` does not use `Syy - Sxy^2 / Sxx`, which cancels when the points
lie almost on a line: it computes each residual `dy - slope * dx` in
double-double, centers the residuals on their mean (which removes the
shift that the rounding errors of the two means add to every residual),
and sums their squares. `SLOPE` skips the residuals.

**D-variance functions**: `DVAR`, `DVARP`, `DSTDEV` and `DSTDEVP` fold
their values through `MomentsAggregate`, so they return the same results
as `VAR.S`, `VAR.P`, `STDEV.S` and `STDEV.P`, including when the sum of
squared deviations overflows.

Behaviour changes besides precision: `STEYX` returns the standard error
instead of `#NUM!` or `0` for points that lie almost on a line
(`=STEYX({0.25,0.4,0.5499999999999999},{0.1,0.2,0.3})` returns 2.83e-17,
the exact value for the stored doubles, which are not evenly spaced,
where develop returns `#NUM!`;
`=STEYX({100000000,200000000,300000000.00000024},{1,2,3})` returns
9.733e-8 where develop returns `0`); `SLOPE` and `STEYX` return
`#DIV/0!` with the message "All x values are equal." (the new
`ErrorMessage.EqualXValues`) when all the x values are equal, as Excel
and Google Sheets return `#DIV/0!` (develop returns `#NUM!` or an
arbitrary number), and finite results where the sum of squared
deviations of the x values overflows or underflows
(`=SLOPE({1,2},{1e-170,2e-170})` returns 1e170; develop returns
`#NUM!`). `DVAR`, `DSTDEV` and `COVARIANCE` return finite results where
only an intermediate sum overflows (`=DSTDEV` of `-1.2e154, 0, 1.2e154`
returns 1.2e154, like `STDEV.S`).

`AVERAGE`, `AVERAGEA` and `SUBTOTAL` 1/101 no longer use
`MomentsAggregate`: they reduce an `AverageResult` (a plain sum and a
count, moved from `ConditionalAggregationPlugin` to its own module)
under their own cache keys (`_AVERAGE` and `_AVERAGE_A`), so they do not
compute the double-double sums. Each argument is evaluated once, and the
results are the same as before.

### Accuracy

The reference is the exact result for the stored doubles, reported as
LRE (digits of agreement, McCullough 1998). Exact references: BigInt
rational arithmetic, Python's `statistics` (exact fractions) and mpmath
at 60 digits. The data is 24 datasets, including all 9 NIST StRD
univariate sets. Measured for `VAR`/`STDEV` at 7a88902 and not
re-measured since; the variance algorithm is the same apart from the `S1
* (S1 / n)` evaluation and the power-of-two exponent, which is exact
scaling and is 0 for all 24 datasets.

| | min LRE over 24 datasets |
|---|---|
| this PR | **15** |
| Gnumeric 1.12.56 | 15 |
| R 4.3.3 `sd()` | 4.7 |
| NumPy 2.5.3 / SciPy 1.18.1 | 0 |
| plain two-pass | 0 |
| Excel (two-pass; 50 identical values → 7.5e-8, exact 0) | 0 |
| Welford / Chan pairwise | 2.4 |
| before (one-pass) | −1 (negative variance) |

Double-double arithmetic does not guarantee correct rounding, so the
JSDoc states the accuracy as about one unit in the last place. Split
arguments, `=STDEV.S(A1:A50, A51:A100)` in either order, hold the same
accuracy. When a range reuses the cached aggregate of a smaller range or
the values come in several arguments, partial aggregates are composed in
a different order than the left-to-right fold of the D-functions, so the
results can differ from them in the last bits.

Compared with Excel: near 1e7 the results are equal, but near 1e11
Excel's two-pass result differs from the exact value in the 4th
significant digit (0.000999538 vs 0.000999460), and for 50 identical
values whose mean is not exactly representable Excel returns 7.5e-8
instead of 0. This PR returns the exact values.

### Alternatives measured

- **Plain two-pass** for the `MomentsAggregate` functions: mean first,
then deviations, which needs every value again. It cannot use the range
cache, and a running `=STDEV.S(A$1:An)` over 5,000 rows became 10x
slower to build and 38x slower to recalculate after one edit. The
functions that already take whole arrays (`DEVSQ`, `COVARIANCE.*`,
`SLOPE`, `STEYX`, D-functions) use two passes, in double-double.
- **Shifted sums plus a term that reproduced Excel's rounding of the
mean**: composable, but it inherited Excel's error (min LRE 0) and
depended on argument order.
- **Compensated (Neumaier) shifted sums**: min LRE 13.1 at about the
same cost as double-double.

### How did you test your changes?

- The tests PR (handsontable/hyperformula-tests#70) adds 118 tests to
the spec of each affected function; 63 of them fail on develop. Expected
values are the exact results for the stored doubles, cross-checked with
Python's `statistics`, compared as a ratio within 1e-14 with
`smartRounding` off. They cover every affected function, the legacy
`STDEV` name, `SUBTOTAL` 7 and 11, two ranges as separate arguments,
range + scalar, a range extending a cached smaller range, two cached
ranges kept at different scales (tiny spreads of different sizes, and
tiny values with huge ones) combined in either order, recalculation
after an edit, the NIST NumAcc4 dataset, identical values (also near
1e300), variances near 1e300, `STEYX` on points exactly and almost on a
line, `SLOPE`/`STEYX` with equal x values (including the error message),
and overflow: a squared deviation close to the largest double in either
order, shifted sums that overflow, two cached ranges whose combination
overflows, a running total that overflows, a value equal to the largest
double, products of deviations near the largest double that cancel, the
sum of squared x deviations overflowing or underflowing in
`SLOPE`/`STEYX`, the D-functions and `COVARIANCE` matching `VAR`/`STDEV`
when the sum of squared deviations overflows, tiny spreads in `STDEV.S`
and `DSTDEV`, products of large and tiny values that cancel in
`COVARIANCE` and `SLOPE`, an overflowing slope in `STEYX`, the same
variance for the values in any order and for a range split at any point
when the squared deviations from the first value overflow, a finite
variance whose sum of squared deviations overflows, `#NUM!` when the
variance exceeds the largest double, `AVERAGE` and `VAR.S` and
`AVERAGEA` and `VARA` over the same range, and `AVERAGE`, `AVERAGEA` and
`SUBTOTAL` 1 evaluating each argument once.
- Full suite (after merging develop): 6733 passed, 0 failed, 3 skipped.
- `STEYX` on points almost on a line was checked against exact rational
results: 4,000 datasets (n from 3 to 12, intercepts, slopes and x values
over 30 to 40 orders of magnitude, residual sums of squares down to
1e-37 of the sum of squared y deviations), largest relative error
1.8e-15, plus 60k datasets of the general `COVARIANCE`/`SLOPE`/`STEYX`
fuzz with no `STEYX` result off by more than 1e-12.
- The double-double helpers were checked against exact BigInt
arithmetic: about 31M cases each for `twoSum` and `twoProduct` (boundary
grids around 2^996, 2^1023, the largest double, subnormals, signed
zeros, infinities and NaN, plus random bit patterns) with no inexact
result where an exact one is representable, and about 5M cases each for
the two divisions.
- An engine differential test against develop and exact results (about
2.9M formula evaluations over ordinary data, large means with small
spreads, extreme magnitudes, and the same data as one range, two ranges,
scalars and a cached range extended) found no case where develop is
finite and this PR returns an error, apart from the ones listed under
"Not in this PR".
- A differential test of the power-of-two exponent against exact BigInt
rational results: 24,000 datasets (magnitudes from 1e140 to the largest
double mixed with zeros and subnormals, opposite-sign values near the
largest double, large means with small spreads at every exponent,
identical values, variances near the overflow threshold; n from 2 to
200), each evaluated in about 20 forms (argument orders, split ranges,
range + scalar, cached prefix ranges), 10.8M checks in all, for `VAR.*`,
`STDEV.*`, `VARA`, `VARPA`, `STDEVA`, `STDEVPA` and `SUBTOTAL` 7, 8, 10
and 11. Where the exact variance is a normal double, the largest error
is 1 ulp (3 results at 2 ulp just above 2^-1022; largest relative error
3.75e-16), all forms agree within 2 ulp, and `#NUM!` is returned exactly
when the exact variance rounds to infinity.
- Benchmark against develop at 92ebfbd (Apple M4 Pro, Node 24; fresh
process per run, 7 alternating runs, warm median): one `STDEV.S` over
100k cells 14.5 → 18.5 ms (1.28x), `AVERAGE` plus `VAR.S` over the same
20k-cell range 5.6 → 7.2 ms (1.29x), recalculation of a running
`STDEV.S(A$1:An)` over 5k rows after one edit 12.9 → 13.9 ms (1.08x),
building a running `STDEV.S(A$1:An)` or `AVERAGE(A$1:An)` over 5k rows
1.00x and 0.99x. Data that needs the exponent (values near 1e150) adds
about 6% to the 100k `STDEV.S`.
- The power-of-two scaling of the D-functions, `COVARIANCE`, `SLOPE` and
`STEYX` was checked against exact BigInt results and against the
previous head (92ebfbd): about 18k random datasets with 23 formulas
each, 18k more from an independent generator, an 80k-dataset
`MomentsAggregate` composition fuzz and 19,664 equal-x cases. No result
differs from 92ebfbd on ordinary data, the D-functions equal
`VAR`/`STDEV` in every regime, and equal x values always give `#DIV/0!`.
On 100k ordinary values, every affected function is within about ±3% of
92ebfbd, and `DVAR`/`DSTDEV` are about 5% faster.
- Heap after building a running formula over 5k rows: `STDEV.S` 10.49 →
11.82 MB, `AVERAGE` 10.48 → 10.96 MB. A cached `MomentsAggregate` is
larger than on develop (two double-doubles, a shift and an exponent); a
cached `AverageResult` is smaller than develop's aggregate.

### Not in this PR

- `VAR.*` results below 2^-1022 (spreads below about 1e-154) are
subnormal numbers with few significant bits; they are rounded at the
scale of the sums and then scaled back, so they can be 1 subnormal ulp
off.
- `CORREL`, `PEARSON`, `RSQ`, `Z.TEST`, `T.TEST`, `F.TEST`, `SKEW` and
`SKEW.P` still compute their sums of deviations in plain doubles
(jStat), so they keep the precision loss on data with a large mean.
- When an intermediate sum overflows, `COVARIANCE` and `SLOPE` scale
large arrays down, which rounds away values about 2^1400 times smaller
than the largest one. On extremely ill-conditioned data (values near the
largest double that cancel, or data spanning about 600 orders of
magnitude) they can then return a finite but inaccurate result where
develop returned `#NUM!`; no double-double method can resolve these.
- `STEYX` rounds twice (division, then square root), so it can be about
1 ulp off.
- Double-double edge cases that predate this change:
`divideDoubleDoubles` with a dividend below about 2^-950 can be a few
ulps less accurate than plain division, and sums or quotients within
half an ulp of the largest double return infinity instead of the largest
double.
- The precision of `AVERAGE`: `=AVERAGE(1e16, 1, -1e16)` returns `0`, as
on develop and in Excel.
- `INTERCEPT` is not implemented in HyperFormula.

### Types of changes
- [ ] Breaking change (a fix or a feature because of which an existing
functionality doesn't work as expected anymore)
- [ ] New feature or improvement (a non-breaking change that adds
functionality)
- [x] Bug fix (a non-breaking change that fixes an issue)
- [ ] Additional language file, or a change to an existing language file
(translations)
- [ ] Change to the documentation

### Related issues:
1. Tests: handsontable/hyperformula-tests#70
2. Merged into this branch: #1798 (`DEVSQ`, `COVARIANCE`, `SLOPE`,
`STEYX` and the D-variance functions)

### Checklist:
- [x] I have reviewed the guidelines about [Contributing to
HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html)
and I confirm that my code follows the code style of this project.
- [x] I have signed the [Contributor License
Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2).
- [ ] My change is compliant with the
[OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html)
standard.
- [ ] My change is compatible with Microsoft Excel.
- [ ] My change is compatible with Google Sheets.
- [x] I described my changes in the
[CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md)
file.
- [ ] My changes require a documentation update.
- [ ] My changes require a migration guide.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core numeric paths for many spreadsheet functions; behavior
changes (errors, edge magnitudes) are intentional but affect formula
results users may rely on.
> 
> **Overview**
> Fixes **catastrophic cancellation** in variance, standard deviation,
and related statistics when the mean is large relative to the spread
(e.g. `STDEV.S` on values near `1e7` with tiny differences).
> 
> Introduces **double-double arithmetic** (`doubleDouble.ts`) and two
aggregation layers: **`MomentsAggregate`** for composable
`VAR`/`STDEV`/`SUBTOTAL`/database `DVAR`/`DSTDEV` (shifted sums with
power-of-two scaling), and **`deviationSums.ts`** for `DEVSQ`,
`COVARIANCE`, `SLOPE`, and `STEYX`. **`STEYX`** now sums squared
residuals per point (with centered residuals) instead of a formula that
cancels on nearly collinear data.
> 
> **`SLOPE`** and **`STEYX`** return **`#DIV/0!`** with *"All x values
are equal."* when all x values match. **`AVERAGE`** / **`AVERAGEA`** use
a lightweight **`AverageResult`** (sum + count) instead of the variance
aggregate. Release script step 3 reads **`CURRENT_VERSION`** after the
step label.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
91456eb. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
…te it

The code-freeze step 8 repointed demo URLs only in docs/guide and
docs/index.md, so the StackBlitz link in README.md stayed on 3.4.x. The
step now scans README.md too, and step 9 stages it, so the rewrite is
committed with the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNPv319TtBWAMhLYXNjoLS
check:licenses excluded hyperformula@3.1.1 from its own license check, so
since 3.1.1 it failed on the package itself (GPL-3.0-only is not on the
allow list). license-checker matches exclusions by exact name@version
only, so the code-freeze step 3 now sets the exclusion to the new version
next to the version bump, skips it when it already matches, and warns if
the script no longer carries the exclusion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNPv319TtBWAMhLYXNjoLS
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hyperformula-docs fe16eed Commit Preview URL

Branch Preview URL
Oct 08 2026, 09:16 AM

sequba commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Vendored parser / drift is red, and the cause is outside this PR.

The job fails with HTTP 404 - access, not drift:

FAIL  could not list handsontable/license-key/vendor/entitlement-key-reader at commit 124660718: HTTP 404 - access, not drift.

The LICENSE_KEY_REPO_TOKEN secret can't read handsontable/license-key. The same failure happens on develop (runs for #1728 and #1784), and has since #1728 merged. This PR only triggers the workflow because its diff against master includes src/license/handsontable-license-key-parser/, which arrived with #1728. None of this PR's commits touch that directory.

There is no fix to port: it needs a token with read access to handsontable/license-key, set as the repository secret. Until then, nothing has checked the vendored reader against the pinned 5.1.1 tag. Someone with access should run npm run check:license-key-parser-drift locally before publishing 3.5.0. I haven't re-run the job, since it fails the same way on every run.


Generated by Claude Code

…changelog

MAXPOOL and MEDIANPOOL (#1718):
- The catalogue entries and the JSDoc said that a window_size or stride
  that is not a positive integer returns #VALUE!; it returns #NUM!.
- A stride greater than window_size now works in a cell (it returned
  #VALUE! in 3.4.0, while calculateFormula() returned the result). The
  catalogue, the JSDoc and a new Changed entry describe it, and the
  isPoolWindowFittingInputArray() JSDoc no longer says the windows tile
  the range.
- The Fixed entry now names both errors and the RangeError it replaced.

Also:
- STEYX: replace the garbled short description.
- Changelog: name COVARIANCE.P and COVARIANCE.S instead of COVARIANCE,
  and list DEVSQ in the very-small-values fix.

The release notes mirror the changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNPv319TtBWAMhLYXNjoLS

sequba commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

FOSSA License Compliance reports "1 issues found" on every commit of this PR, and I can't tell what it flags.

It is red on 00c0dd7, 60734d6 and fe16eed. The first of these came before the check:licenses change, so that change isn't the cause. Reading the report needs access to app.fossa.com, which I don't have.

What I checked:

Someone with FOSSA access needs to open the report linked from the status and say which dependency or file it flags. Once we know, I'll fix it on this branch, or it can be ignored in FOSSA if it's acceptable.


Generated by Claude Code

@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.61977% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.28%. Comparing base (ebaa2b2) to head (fe16eed).
⚠️ Report is 13 commits behind head on master.

Files with missing lines Patch % Lines
src/interpreter/plugin/MomentsAggregate.ts 97.72% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1801      +/-   ##
==========================================
- Coverage   97.31%   97.28%   -0.04%     
==========================================
  Files         195      207      +12     
  Lines       15734    16218     +484     
  Branches     3456     3588     +132     
==========================================
+ Hits        15312    15777     +465     
- Misses        414      433      +19     
  Partials        8        8              
Files with missing lines Coverage Δ
src/ArraySize.ts 100.00% <100.00%> (ø)
src/BuildEngineFactory.ts 100.00% <100.00%> (ø)
src/Config.ts 94.64% <100.00%> (+0.52%) ⬆️
src/Emitter.ts 100.00% <ø> (ø)
src/HyperFormula.ts 97.40% <ø> (-2.35%) ⬇️
src/Operations.ts 98.94% <100.00%> (+<0.01%) ⬆️
src/error-message.ts 100.00% <100.00%> (ø)
src/errors.ts 100.00% <100.00%> (ø)
src/helpers/licenseKeyValidator.ts 100.00% <100.00%> (+9.67%) ⬆️
src/i18n/languages/csCZ.ts 100.00% <ø> (ø)
... and 41 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Performance comparison of head (fe16eed) vs base (af2d59d)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |  454.31 |  449.68 | -1.02%
                                      Sheet B |  143.05 |  141.73 | -0.92%
                                      Sheet T |  124.32 |  124.23 | -0.07%
                                Column ranges |  581.57 |  586.36 | +0.82%
                                Sorted lookup | 16892.2 | 17396.6 | +2.99%
Sheet A:  change value, add/remove row/column |   12.39 |   13.35 | +7.75%
 Sheet B: change value, add/remove row/column |  118.72 |  116.17 | -2.15%
                   Column ranges - add column |  153.87 |  151.33 | -1.65%
                Column ranges - without batch |  484.99 |  470.79 | -2.93%
                        Column ranges - batch |  120.81 |  120.44 | -0.31%

@sequba
sequba merged commit 99a45ea into master Oct 9, 2026
54 of 57 checks passed
@sequba
sequba deleted the release/3.5.0 branch October 9, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants