Skip to content

HF-307: align getRegisteredFunctionNames with the license gate, deprecate its static form - #1743

Open
marcin-kordas-hoc wants to merge 33 commits into
hf-307-entitlement-gating-pr3from
hf-307-registered-function-names
Open

marcin-kordas-hoc wants to merge 33 commits into
hf-307-entitlement-gating-pr3from
hf-307-registered-function-names

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Update 2026-09-18 — rebased onto the collapsed engine/tests PRs

hyperformula#1730 and hyperformula-tests#32 were folded from five PRs each into one, so this
PR's base moved to #1730. Nothing in this PR's own diff changed; the "9/9" numbering and the
#1741 base reference below are stale — the stack is now #1728 → #1729 → #1730 → this PR.


9/9 of the HF-307 stack, stacked on #1741. Pairs with hyperformula-tests#43merge the tests PR first. Finishes decision D2 on the one API it missed.

The problem

getRegisteredFunctionNames() returned the whole catalogue under a restricted key (measured: 422 names on a calculated-fields key, including functions that evaluate to #LIC!). A function picker built on it has exactly the problem #1724 (HF-349) and #1731's metadata filter exist to prevent. Flagged in #1731's "found, not fixed here" section; this closes it.

What changed

Instance method — now lists exactly what the instance can evaluate, sharing the one licenseListsFunction rule with the interpreter and the metadata API so the three surfaces cannot drift:

  • reads getListableFunctionIds() instead of getRegisteredFunctionIds() — the protected built-ins are included uniformly (OFFSET was missing before; getAvailableFunctions already listed it);
  • reads config.translationPackage — the instance's own snapshot — instead of a fresh global getLanguage lookup, which can report a localized name the instance refuses to evaluate, and which throws once the host unregisters that language code;
  • filters by the license, with the invariant intact: a missing, invalid, or expired key does not shorten the list.

Static method — deprecated, not removed. See below; this is a change from the first version of this PR.

The static: deprecated rather than removed (changed after review)

The first version of this PR deleted HyperFormula.getRegisteredFunctionNames(code), citing HF-349 as precedent. That was wrong, and the review caught it:

$ git show 3.4.0:src/HyperFormula.ts | grep -c "public static getRegisteredFunctionNames"   # 1

The method is in the released 3.4.0 tag, and HF-349's own commit message says its removal was free precisely because "Both methods are unreleased, which is the only free moment to remove them." So the precedent does not extend here: deleting it would be a breaking change in a minor release, against the Semantic Versioning this project states it follows, and DEV_DOCS's Definition of Done would require a migration-guide section that docs/guide/ has no 3.x home for.

So it is now @deprecated with the wording this repo already uses for that situation (arraySizeMethod / arrayFunction in 3.1.0: "deprecated and will be removed in one of the next major releases"), plus a Deprecated changelog entry. Overturnable in one comment if you would rather take the break now — the direction is Kuba's D2 either way; only the timing changed.

The deprecation notice is explicit that the two are not interchangeable: the static translates into any registered language without an engine, so migrating means building one (HyperFormula.buildEmpty({ language: 'plPL' }).getRegisteredFunctionNames()).

D2's precondition, discharged

Kuba's D2 answer made the removal conditional: "perhaps we can remove the static methods, I'll check which methods does formula-builder use". That check is done — searched the org, and both consumers call the instance form, not the static one:

Consumer Call site Form
formula-builder packages/core/src/engine/functionCatalog.ts:81this.engine?.getRegisteredFunctionNames?.(), in try/catch, declared optional in engine/types.ts:66 instance
aurasheet src/core/FormulaEngine.ts:97this.hf.getRegisteredFunctionNames() instance

Both use it to build a function picker (functionCatalog.ts; FormulaAutocompletePlugin.ts) — the surface #1724's rationale was about — so the license filter added here improves both rather than disturbing them. Under gpl-v3 or any unrestricted key their lists are unchanged.

Spec-to-ship review (2026-08-20): also fixed here

  • The translation-snapshot change was unpinned. Mutation-verified: reverting it to the global lookup left 309 tests green, even though the commit message and the new JSDoc both name it. Now pinned by a test that unregisters the language after the engine is built — the scenario that distinguishes the two implementations, since getLanguage throws for an unregistered code while the snapshot keeps working.
  • The licence guide listed only two of the three narrowing methods. It now names this one too, in both places.
  • The new JSDoc over-claimed parity with getAvailableFunctions: for a function whose translation is the empty string this method returns '' while that one falls back to the canonical id. The claim is now scoped to the ids and the licence rule, with the naming difference stated.
  • Reverting the static removal also removed the docs-build change it required, so docs/.vuepress/config.js is untouched by this PR again (it no longer builds one engine per documentation page).

Testing

New suite pins the alignment in both directions (agrees with getAvailableFunctions name for name; narrows on a restricted key; never narrows on a bad key; aliases gate with their canonical; answers from the instance's own snapshot). Full private suite: 517 suites / 6462 passing, 3 pre-existing skips; tsc --noEmit and ESLint clean.

Note for review

functions-metadata.spec.ts now skips listed ids with no plugin: OFFSET is listed (it is callable) but parse-time resolved, so it legitimately has no registry metadata.

🤖 Generated with Claude Code

https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o

The codecov/project dip, traced

codecov/project was red at −0.02% while codecov/patch reported 100% of the diff hit. Rather than
write that off as a threshold artifact, I measured coverage on this commit and on its base and
diffed the per-file numbers:

file base (8/9) this PR, before the fix
src/HyperFormula.ts 674 / 675 675 / 676
src/interpreter/FunctionRegistry.ts 130 / 130 129 / 130
total 12 933 / 13 268 12 933 / 13 269

So the covered count did not move and one previously covered line stopped executing — a real
consequence of this change, not a rounding artifact. The line was the instance
FunctionRegistry.prototype.getRegisteredFunctionIds(), whose only caller in the whole repository
was the method this PR rewrites. Nothing in src/, nothing in the private suite, and nothing
outside (the class is not exported from src/index.ts) calls it any more.

Removed, since this change is what orphaned it. The static FunctionRegistry.getRegisteredFunctionIds()
is untouched and still used — by the deprecated static method above and by three specs.

For the record, the single uncovered line left in HyperFormula.ts is pre-existing and not mine:
removeNamedExpression's unreachable return [], which already carries a codecov note comment
explaining why it cannot be hit.


Note

Medium Risk
Changes public API behavior for function pickers under restricted keys and deprecates a shipped static method; evaluation logic is unchanged but listing surfaces must stay consistent with licensing.

Overview
Instance getRegisteredFunctionNames() now lists only functions the engine can evaluate under the current license—the same rule as getAvailableFunctions() / getFunctionDetails(). It uses listable ids (protected built-ins like OFFSET included), filters with licenseListsFunction, and reads names from the instance translationPackage snapshot instead of a global language lookup. Missing, invalid, or expired keys still do not shorten the list.

The static HyperFormula.getRegisteredFunctionNames(language) is deprecated (kept for semver because it shipped in 3.4.0), with docs and changelog pointing callers at the instance API—e.g. HyperFormula.buildEmpty({ language: 'plPL' }).getRegisteredFunctionNames().

Internal cleanup: instance FunctionRegistry.getRegisteredFunctionIds() is removed as unused. License docs and changelog document the third narrowed listing API and the deprecation; changelog also adds a retroactive 3.4.0 note that static metadata helpers were removed.

Reviewed by Cursor Bugbot for commit fab1a6f. Bugbot is set up for automated code reviews on this repo. Configure here.

marcin-kordas-hoc and others added 9 commits August 18, 2026 08:53
Ports the read side of the typed license key format into src/license/vendor/ as
TypeScript (allowJs is off and strict is on, so this is a port rather than a
copy). Nothing consumes it yet - the key-to-entitlement adapter follows in the
next commit.

Vendored: constants, the default schema, the six reader-side helpers of utils,
the pure-JS SHA-512, and the key-data extractor. Not vendored: key generation
and the schema validator, which are unreachable here because HyperFormula only
ever reads keys and always reads them with the default schema.

The delivery form follows the key spec's own recommendation of a vendored copy
with a drift check, rather than a shared package: a private dependency would
break npm install for open-source users of this GPL package.

PROVENANCE.md records the upstream commit and a per-file sha256 of the upstream
sources, so drift is detectable by re-cloning and re-hashing, and lists the
deliberate divergences - notably that the extractor drops the custom-schema
parameter and additionally returns licensedProductName, since the grace period
lives on the licensed product entry and re-deriving "the first schema product
present in the payload" in the caller could drift from the rule used to derive
the expiry.

Payload fields are typed unknown: field types are checked when a key is
generated, which constrains nothing about a payload that reaches this code, so
consumers must narrow rather than trust a declared shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
Before this commit a genuine typed key did not work at all. The validity check
recognizes three fixed strings and the older 25-character format; a typed key
matched none of them and fell through to INVALID, so every formula returned
#LIC! and the console warned that a paid-for key was invalid. Verified by
building an engine with a real, unexpired subscription key before touching
anything.

resolveLicense reads the key once and answers both gates from that single
reading, so they cannot disagree about what the string says. A typed key is
recognized first; anything else - gpl-v3, an older-format key, an empty string,
a typed key with a broken checksum - falls through to checkLicenseKeyValidity
completely untouched. That is what keeps existing behaviour bit-identical: the
existing function is not modified, only extracted from (notifyLicenseKeyState),
so both paths report the same states with the same wording and share the
one-warning-per-page flag rather than each getting their own.

Expiry follows the format's own rules: a key with no expiration date never
expires; trial and subscription keep working for `grace` days past an inclusive
expiration date, against the clock; a perpetual key compares its maintenance end
against the build release date, so an air-gapped install with a wrong clock is
unaffected. An unknown release date resolves to "not expired", matching what the
existing validator already does - a build that cannot tell its own age must not
start rejecting keys customers paid for.

The invariant this PR must not break is enforced here and mutation-tested: only
a VALID typed key resolves to a restricted entitlement. Missing, invalid and
expired all resolve to unrestrictedEntitlement(), for typed keys exactly as for
the older format. Gate A already stops formula evaluation on its own; letting a
bad key restrict the entitlement as well would make PR 2's ensureCapability
throw from the CRUD API, turning today's "formulas fail, the API still works"
into a silent breaking change for every user whose key lapsed.

Full unit suite green (6260 tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
Replaces the single-core-token placeholder with the four function packages of
the packaging design, and teaches the adapter both payload shapes.

The membership is transcribed from that design's own per-function evidence file
rather than invented. The transcript was checked by reproducing the file's five
published counts exactly: 370 rows, and 17 / 51 / 127 / 355 cumulative plus 15
operators. Coverage was checked the other way too - all 423 registered function
ids resolve into the 370 canonical entries once HF's 53 declared aliases are
canonicalised, with zero uncovered, which is also what makes the rule that
aliases travel with their canonical function true here for free.

THIS MEMBERSHIP IS A DRAFT and is marked as such in the source. The packaging
design is still under review, with the free tier's exact contents and the
placement of several function families not yet settled. Landing it now is a
deliberate call, not a claim that it is final. capability-table.spec.ts pins the
counts so a later edit cannot drift from the evidence silently.

Both payload shapes are read, per product entry, by detecting `capabilities`:
the shipped shape (tier/addons/exp/grace, contract type from the key tag) and
the newer specified shape (capabilities/usage_until/release_until/notice/flags,
with no commercial vocabulary in the payload). The two disagree about nearly
every field, the newer one is still under review, and only the first can be
minted today, so reading both means an already-issued key keeps working
whichever way that is settled. The newer spec also contradicts itself on whether
its dates are YYYY-MM-DD strings or numeric timestamps, so both are accepted.

Commercial tier names are translated to capability tokens in the adapter, not
mirrored into the table, so the table speaks one vocabulary. An unknown tier
passes through untranslated and surfaces as an unrecognized capability rather
than being swallowed.

Grants are stored fully expanded rather than chained through `implies`: the
design states the enforcement layer must not assume a hierarchy between tokens.
Operators are granted by the core token as engine baseline, and the protected
built-ins OFFSET and VERSION are listed nowhere, since the interpreter never
gate-checks them.

Features are all still granted by the core token. The evidence covers functions
only; nothing has decided whether undo/redo or the clipboard is a paid feature,
and restricting one here would both invent a product decision and make PR 2's
ensureCapability start throwing from the CRUD API for real keys.

Full unit suite green (6272 tests). Table membership mutation-tested: moving one
function across a package boundary fails the gating tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
…l-open dates

Bugbot and a code-review pass found real defects in the previous three commits.
Each was reproduced before fixing and is now pinned by a test.

TWO CRASHES on checksum-valid keys. A key whose HyperFormula entry was `null`
rather than an object threw "Cannot read properties of null (reading 'tier')"
straight out of the Config constructor, and a numeric date outside Date's range
threw "Invalid time value" from toISOString. Both killed engine construction,
where every other malformed key merely resolves to INVALID. Every payload field
is untrusted; nothing may assume a shape now.

CUSTOM FUNCTIONS WERE GATED. `functions_4` was filled from the function registry
at run time, which swept in anything registered through registerFunctionPlugin -
putting a user's OWN function into the most expensive package and returning
#LIC! for it on every smaller licence, the opposite of decision D1. The
excel-simulator set is now enumerated statically like the other three, so the
whole table is static and a function it does not list is not gated at all,
which is exactly the treatment a custom function should get. The cost is that a
newly implemented built-in is ungated until added here, which the completeness
invariant fails on - a much better failure mode.

FAIL-OPEN DATES. An unreadable rev-5 date resolved to "never expires", turning a
minting typo into a permanent licence, while the shipped shape already rejects a
malformed `exp`. A present-but-unreadable date now invalidates the key. String
dates go through the vendored parseIsoDate, so `2027-02-30` is rejected rather
than rolling over into March and granting two extra days.

WRONG SOURCE FOR REV-5 TERMS. Dates, notice and grace were read from the
licensed product entry for both shapes, but that rule belongs to the shipped
shape; under rev 5 every product entry carries its own terms. HyperFormula now
reads its own under rev 5, and flags no longer disagree with the rest.

Also: the expired-on date now reports the first day NOT covered, the convention
the legacy validator already uses, so the two paths no longer differ by a day.

Corrected a comment that the capability-table commit had invalidated: the core
token grants operators and the API surface, NOT a usable function set, so a key
whose tokens this build does not recognize evaluates operators only and returns
#LIC! for every function, silently. That cliff is deliberate per D3 but severe;
it is now described accurately and flagged for review rather than misdescribed.

Two review findings were checked and rejected: the package arrays total
16/50/125 rather than the documented 17/51/127 because OFFSET and VERSION are
protected and deliberately excluded, and INT is an excel-simulator function in
the evidence, not a math-engine one.

Full unit suite green (6305 tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
formatDate used local getters on a date built at UTC midnight, so anyone west of
UTC saw a console warning naming the day BEFORE the one their key carries.

This is pre-existing rather than new - the legacy path builds its date the same
way, from a whole number of days since the epoch - so fixing the shared helper
corrects both paths rather than leaving two conventions. No test asserts the
message text, and nothing else calls formatDate.

Verified by running the same expired key under TZ=Pacific/Midway (UTC-11),
TZ=UTC and TZ=Pacific/Kiritimati (UTC+14): all three now print "January 2,
2020" for a key whose exp is 2020-01-01, which is the first day NOT covered -
the convention the legacy path already used.

Deliberately not covered by a test: the only observable is console.warn, and
the warn-once flag is module-level and never reset, so such a test would fire
only when it happened to run first in the module registry. An order-dependent
test is worse than none here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
…swers 12.08

Two changes from Kuba's answers on the task (comment of 12.08):

"Feature gating should work, but the legacy keys should grant all feat:*
capabilities." An earlier revision granted all five features from CORE_TOKEN,
which made feature gating inert by construction - no typed key could ever lose
an API area. The five features now live on their own feat:* tokens (spelled
after the task's draft vocabulary), and core grants the operators alone. A
rev-5 key states its feature grants explicitly; the shipped shape - whose
vocabulary predates feature tokens and whose tiers are products sold with the
full API - is granted all five by the adapter, so an existing shipped-shape
key's API behaviour is unchanged. Legacy keys resolve to the unrestricted
entitlement, which is the carve-out Kuba named, already in place.

"One unrecognized token currently silences the ENTIRE key - this seems like an
implementation error." Confirmed and decoupled: silence now comes solely from
the key's flags. The coupling suppressed strictly more than D3 asks for - a
vocabulary mismatch would have swallowed expiry notices too.

The #LIC! cliff comment is updated to record D6-A: Kuba ratified D3 as-is
("this situation should never happen. There is no point in issuing a key if
empty capabilities.").

Tests: handsontable/hyperformula-tests#32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
…honoured

Three fixes, all from reading key spec rev 5 (CU doc 8cnjcyf-31675 page
8cnjcyf-48155, updated 12.08) against the code and running the result.

**Feature tokens are OPT-IN, not opt-out.** The previous revision granted the
five feature areas only in the shipped-shape branch, so a key was denied every
gated API area unless it explicitly named `feat:*` tokens. Two key classes that
myHOT can mint TODAY do exactly that:

- rev-5 keys. §2.2 lists HyperFormula's whole token vocabulary as `functions_1..4`,
  `spreadsheet`, `import_export` - there is NO `feat:*` entry at all. Minting the
  spec's own §2 example payload and running it: setCellContents, addRows, copy,
  undo, addNamedExpression and batch ALL threw.
- shipped-shape keys whose payload carries no usable `hyperformula` entry, i.e.
  Handsontable-only keys and keys with `hyperformula: null`. These fell outside
  the branch that did the granting, so they lost the API that `core` used to give
  them - and, being gate-A VALID, they lost it without even a console warning.

So absence of a `feat:*` token cannot mean "no features": no vocabulary in
circulation can express one. It means "this key does not talk about features",
and the task's additive-safety rule - a grant may grow, never shrink - makes the
whole gated API the only safe reading. A key that DOES name a `feat:*` token
still gets exactly the areas it names, which is what Kuba asked for ("Feature
gating should work").

**`no-console-warns` is honoured.** rev 5 is not self-consistent about the flag:
its normative table and example payload (§2.3, §2) say `no-console-warns`, its
runtime sections (§4.3, §5.2) say `silent-console`, earlier revisions said plain
`silent`. Only the last two were recognised, so a doc-conformant SaaS key printed
console warnings it had explicitly asked to suppress. All three now count.

**An unreadable `capabilities` rejects the key.** `capabilities` present but not
an array fell through to the shipped-shape branch, which was a free pass twice
over: the key gained every feature it never carried, and its rev-5 dates were
never read, so a subscription expired in 2020 resolved as perpetual. It now
returns null (INVALID), matching what the module already does for an unreadable
date and what its own doc comment promises.

Also adds `resetLicenseKeyNotificationForTests` (@internal): the warn-once flag
is module-level and never reset, which made the whole console-message path
untestable - deleting the notify call left all 6300 tests green.

Tests: handsontable/hyperformula-tests#32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
`TIER_TO_CAPABILITY_TOKEN` was an object literal, and the tier it is looked up
by comes from the payload - which is attacker-influenced, since a typed key's
checksum is an unkeyed SHA-512 that anyone can compute. An object lookup also
answers for every `Object.prototype` member, so `tier: "constructor"` resolved
to a FUNCTION and `tier: "__proto__"` to an object. Either one landed in the
capability token list, and the `feat:` scan added on 13.08 then called
`.indexOf` on it:

  TypeError: token.indexOf is not a function
    at licenseResolution.ts (Array.some) -> licenseTermsOf -> resolveLicense
    -> new Config -> HyperFormula.buildFromArray

The engine failed to CONSTRUCT. That breaks the rule this module documents and
already honours elsewhere: a malformed key produces an `invalid` verdict, never
a thrown exception. Worth being precise about the history - the unsafe lookup
predates the 13.08 change, but before it a non-string token was merely ignored
by a Map lookup; the opt-in scan is what turned it into a crash.

Fixed by making the map a `Map`, which answers only for keys actually put in it
and matches `CAPABILITY_TABLE`. It also makes the types honest: `Record<string,
string>` told TypeScript the lookup yields a string, which was the lie behind
the crash, while `Map.get` returns `string | undefined`.

Behaviour for such a key is now identical to any other unknown tier: VALID key,
token passed through, recorded as unrecognized, grants nothing (D3).

Tests: handsontable/hyperformula-tests#32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
… not hold

`releaseDateTimestamp` said it reads HT_RELEASE_DATE "so a perpetual typed key
and a legacy key agree on what 'this build' means". They do not, east of UTC.

This function uses `Date.UTC`; the legacy validator parses the same env value
with `new Date(month/day/year)`, which is LOCAL. Measured at process level:

  HT_RELEASE_DATE=10/08/2026        legacy (local)   typed (UTC)
    TZ=UTC, TZ=America/Los_Angeles      20675           20675     agree
    TZ=Asia/Tokyo                       20674           20675     differ
    TZ=Pacific/Kiritimati               20674           20675     differ

Raised by Bugbot on 12.08 and left unanswered for four days while I reported the
PR as review-clean off the check status - which it was not.

The CODE is right and stays. UTC is required for a typed key: key spec rev 5 §1.2
makes offline/online parity a hard rule, and a local clock breaks it. Legacy keeps
its local parse because legacy behaviour is frozen this release - switching it
would move the expiry verdict of already-issued keys by a day for every customer
east of UTC. So the fix is to stop the comment claiming the opposite, and to state
the consequence plainly: two customers east of UTC, one on a legacy key and one on
an equivalent typed key, can disagree by a day about whether this build is covered.
Reconciling them is a product decision.

No test accompanies this, deliberately. The property is not observable in this
suite: assigning `process.env.TZ` mid-run has no effect once the runtime resolved
its timezone (probed - UTC, Asia/Tokyo and Pacific/Kiritimati all returned an
identical timestamp inside Jest), and CI runs in UTC where both parses agree. A
test written that way passes whichever parse the source uses; I wrote one,
mutation-checked it, found it vacuous, and removed it rather than ship an assertion
that cannot fail. Pinning it needs a timezone-parameterised CI job. The reasoning
sits next to the release-axis tests so the gap stays deliberate.

Tests: handsontable/hyperformula-tests#32

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

qunabu commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 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 Updated (UTC)
❌ Deployment failed
View logs
hyperformula-docs fab1a6f Sep 22 2026, 01:40 PM

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Performance comparison of head (fab1a6f) vs base (f782d0e)

                                     testName |    base |   head | change
-------------------------------------------------------------------------
                                      Sheet A |  488.37 | 500.77 | +2.54%
                                      Sheet B |  153.05 | 159.74 | +4.37%
                                      Sheet T |  137.19 | 145.93 | +6.37%
                                Column ranges |  468.58 | 482.01 | +2.87%
                                Sorted lookup | 14411.9 |  15006 | +4.12%
Sheet A:  change value, add/remove row/column |   14.77 |  16.18 | +9.55%
 Sheet B: change value, add/remove row/column |  128.26 | 139.69 | +8.91%
                   Column ranges - add column |  148.86 | 161.48 | +8.48%
                Column ranges - without batch |  476.64 | 495.01 | +3.85%
                        Column ranges - batch |  116.32 | 121.43 | +4.39%

@marcin-kordas-hoc
marcin-kordas-hoc marked this pull request as ready for review August 20, 2026 12:54
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-307-registered-function-names branch from b727d70 to d314b32 Compare August 20, 2026 13:09
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-307-registered-function-names branch from d314b32 to 0f210ad Compare August 21, 2026 01:32
@marcin-kordas-hoc marcin-kordas-hoc changed the title HF-307: align getRegisteredFunctionNames with the license gate, drop its static form (9/9) HF-307: align getRegisteredFunctionNames with the license gate, deprecate its static form (9/9) Aug 21, 2026
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-307-registered-function-names branch from 0f210ad to dcf9354 Compare August 21, 2026 02:14

@Tobiadefami Tobiadefami left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at dcf9354 together with the paired tests at a406fc76. I checked the instance registry and translation snapshots, license filtering, aliases, custom and protected functions, raw translation behavior, static API compatibility and deprecation, documentation, changelog, and current automated findings. The focused paired run passes 17 suites (317 tests), TypeScript passes, and all current engine checks are green. I found no additional material issue in this PR.

marcin-kordas-hoc and others added 8 commits August 26, 2026 03:08
…grace

`validityOf` computes the real deadline as `usage_until + 1 day + grace`, then reported
`usage_until + 1 day` as the day the key expired, dropping the grace interval entirely.
The comment two lines above already promised "the first day NOT covered", which is the
convention the legacy validator uses, so the code disagreed with its own contract for
every key carrying a positive grace.

Measured on the stack head before the change: a key with usage_until 2027-08-12 and
grace 90 stops working on 2027-11-11, and the console said "expired on August 13, 2027" -
byte-identical to the message the same key produces with grace 0. After the change the
two cases print November 11 and August 13 respectively.

No validity decision reads `expiredOn`; its only consumer is the console message, so this
changes what we tell the customer, not what the gate allows.

Reported by Tobiadefami on this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The licence comments carried two things this repository has no precedent for: the
first name of a colleague attached to business decisions, and the ids of internal
planning documents. Neither is resolvable by anyone reading the published package,
and both were about to enter the public history permanently - a squash merge puts
each branch's final content on develop, so removing them further up the stack would
not have helped.

The substance is untouched: every comment still says what was decided and why, and
the HF-nnn task references stay, since those already appear in this repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getAvailableFunctions` and `getFunctionDetails` read straight from the function
registry, with no license filter, while the interpreter gates the same functions
per call. A restricted key therefore advertised functions that return `#LIC!`
when called - the exact failure removing the static metadata methods (HF-349)
was meant to prevent, left half-delivered because the instance methods never
learned to read the key their rationale said they could.

Both now filter through `licenseListsFunction`, which shares one
`licenseAllowsFunction` rule with the interpreter rather than spelling the same
condition out twice, and canonicalises aliases the same way. Extracting that
rule is the point: two copies would drift, and the drift is invisible until a
customer's picker offers a function that fails.

Gate B only, deliberately - never the validity state. A missing, invalid or
expired key resolves to an unrestricted entitlement (the invariant), so it
reaches the filter with `unrestricted` set and keeps the whole catalogue. That
falls out of the invariant rather than being a second decision, and it is the
useful answer: narrowing to the two protected built-ins would hand an integrator
who has not wired up their key yet an empty function picker and no clue why.
The list narrows only for a *valid* key that genuinely excludes a function.

Also documents `#LIC!` in types-of-errors.md, which listed only key problems and
not "function not in your package", and adds the CHANGELOG entry the feature has
not carried so far - PRs 1-3 were internals by design.

The guide deliberately documents the mechanism, not the package contents: HF-306
is still in review with six open questions, so publishing the lists now would
put moving targets in the public docs.

Tests: handsontable/hyperformula-tests#33

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
Re-derived every function's lowest package straight from the 21 fun:<family>.<A|B|C>
group tokens in CU doc 8cnjcyf-33175/8cnjcyf-47835 and re-partitioned MATH_ENGINE_FUNCTIONS,
CALCULATED_FIELDS_FUNCTIONS, SPREADSHEET_FUNCTIONS and EXCEL_SIMULATOR_FUNCTIONS to match.

No function was added or removed (353 total, before and after) - only reassigned to its
correct tier. The prior table was materially stale: missing 6/22/50 functions at the three
lower tiers respectively, with some (e.g. INT, STDEV.S) sitting a tier too high.

OFFSET and VERSION remain deliberately excluded from every list: both are named by the doc
but are protected built-ins outside the token system today (see hf-306-token-vocabulary-final
memory for the two different root causes and what closing each would take - out of scope here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
A scoped spec-to-ship review of PR 4 (19.08) confirmed 5 findings of 11 raised. Two
are documentation defects in this PR's own new text, and both were measured rather
than read:

- The guide said "Custom functions you register yourself are always available,
  whatever your key grants", and the JSDoc this PR adds to getAvailableFunctions
  repeated it. The actual rule in licenseListsFunction is "not covered by the
  capability table", NOT "not user-registered". Verified on a crm-tier key: after
  registering an own plugin implementing BITAND, getFunctionDetails('BITAND')
  returned undefined and =BITAND() returned #LIC!. An integrator following the
  guide would ship a picker that hides their own function and a sheet that errors.
  Note getFunctionDetails' own JSDoc already called this "the exception", so the
  guide contradicted the API reference it links to - both are corrected here, in
  one pass, because fixing only the guide leaves the JSDoc wrong.

- The guide said a missing/invalid/expired key means "every function evaluates to
  a #LIC! error" and "stops formulas from calculating until you fix the key". Both
  overshoot. Measured with licenseKey: '': =VERSION() returned a version string and
  =OFFSET(A1,0,1) returned 2 - the two protected built-ins are exempt at the
  interpreter's gate - while =SUM(A1:B1) returned #LIC! and =A1+B1 kept calculating.
  Now says "every function call", with a paragraph naming what keeps working, so a
  user whose key lapsed is not told to expect a blank sheet.

No behaviour change in this commit. The paired tests PR carries the two test-level
fixes from the same review (capability-table membership pinned by name rather than
by count only, and the protected built-ins asserted on the LISTING path, not just
on evaluation).

Full private suite: 512 suites, 6383 passed, 3 pre-existing skips, 0 failures.
tsc --noEmit and tsc -p tsconfig.test.json clean; eslint 0 errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
Three corrections to the same claim, all of them user-visible:

- "every function call evaluates to `#LIC!`" is false. `VERSION()` and `OFFSET()` are
  function calls and they bypass both gates, because the interpreter exempts protected
  built-ins before it consults the licence. Under a bad key `SUM()` returns `#LIC!` while
  those two keep evaluating.
- The guide's next paragraph attached `VERSION()`/`OFFSET()` grammatically to "are not
  function calls", which states the opposite of what they are. Operators are not function
  calls; those two are, and are exempt for a different reason. Split accordingly.
- "Methods that only read data never throw" is misleading: `copy()` and `cut()` mutate
  nothing and return values, yet both are gated by the clipboard feature and throw
  without it.

Reported by Tobiadefami on this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The licence comments carried two things this repository has no precedent for: the
first name of a colleague attached to business decisions, and the ids of internal
planning documents. Neither is resolvable by anyone reading the published package,
and both were about to enter the public history permanently - a squash merge puts
each branch's final content on develop, so removing them further up the stack would
not have helped.

The substance is untouched: every comment still says what was decided and why, and
the HF-nnn task references stay, since those already appear in this repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The key's `notice` field was parsed into LicenseExpiry and read by nothing.
Now a VALID typed key whose usage_until lies within `notice` days of the
current UTC instant prints a single console warning naming the expiry date
(UTC marker included). The warn-once identity is the key string, not the
process — two engines built with two different keys each get their own
warning. release_until-axis keys never warn (rev 5: notice/grace have no
effect there), the key's silent flags suppress it, and blocking at/after
expiry is byte-identical to before (Kuba's D5-A: hard stop stays in 3.5.0,
the full rev 5 §4.1 window model is a follow-up).

Trials made this concrete: a trial is just a key with grace=0 and notice>0
whose warnings must surface in the console (packages meeting 12.08).

Implemented by a prep-ship lane (task HF-307-notice-window); verified here:
license suite 165/165 under Jest, tsc --noEmit clean, eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdHPZAjciZFWqGa19Yf7it
marcin-kordas-hoc and others added 12 commits August 26, 2026 03:17
Four findings from the spec-to-ship re-review (each cross-confirmed by at
least two independent review angles):

1. Message wording: the notice now uses rev 5 section 3.2's own subscription
   clause - "is valid until <last covered day> (UTC)" - instead of "will
   expire on". The pre-existing expired message names the first day NOT
   covered (+1 day, frozen convention), so "expires on Aug 25" followed by
   "expired on Aug 26" printed two different days for one boundary.

2. The notice read is gated on the key SHAPE (rev 5), not on the field's
   presence: on the shipped shape the terms come off the LICENSED product's
   entry - for a dual-product key, Handsontable's - so a stray `notice`
   field there must not switch HyperFormula's console output on. The
   expiryWithinNoticeWindow doc also no longer claims kind='usage' implies
   the date came from usage_until (the envelope-exp fallback is real and
   documented as accepted standalone; the entitlement re-port removes it).

3. rebuildWithConfig's transient serialization-only Config no longer prints
   license messages: replacing keyA with keyB used to print keyA's notice in
   the very call that discards keyA. Config gains an internal-defaulted
   notifyLicenseMessages parameter, same pattern as showDeprecatedWarns.

4. The warn-once identity is now trim + the trailing 128 chars (the key's
   own checksum): extractTypedKeyData trims, so 'KEY' and 'KEY\n' are one
   license and must be one identity; truncation bounds a long-lived
   process's memory to 128 chars per distinct warned key. CHANGELOG entry
   gains its PR link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdHPZAjciZFWqGa19Yf7it
… not before its end

The window start was derived from `usageAxisDeadline` (`usage_until + 1 day`), which makes
the window a day shorter than the specification's and opens it a day late. The date-semantics
fixtures are explicit: for `usage_until` 2027-08-12 with `notice` 60 the warning must appear
from 2027-06-13T00:00:00Z, and 2027-08-12 minus 60 days is exactly that day. Section 4.1
counts the window "before `usage_until`", not before the boundary that ends it.

Measured before the change, clean processes with a fixed clock: silent at
2027-06-13T00:00:00Z and still silent through 2027-06-13T23:59:59Z, first warning at
2027-06-14T00:00:00Z. After it, the warning appears exactly at 2027-06-13T00:00:00Z and the
instant before it stays silent.

The same off-by-one cost a trial its first warning day: with `notice` equal to the whole
term (45/45), the window is meant to open on the day the key is issued, and it opened the
day after. Measured on a 2026-09-26 trial: silent on 2026-08-11, warning from 2026-08-12.

The end of the window is unchanged, so a key already past `usage_until` still gets no notice.

Reported by Tobiadefami on this PR, including the trial case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same cleanup as on the branches below: a colleague's first name attached to a
business decision, removed from a comment this branch introduces. The substance of
the comment is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per the 2026-08-12 packages meeting: `spreadsheet` backs the Spreadsheet
Bundle add-on and grants Crud, UndoRedo, Clipboard and Batching ("chyba tez"
batching - Kuba, 12.08); `import_export` grants FeatureId.ImportExport, a
reserved grant with no gated method until HF-107 ships the feature.

Additive only: a key naming neither add-on keeps every feature area it has
today (the opt-in rule for keys carrying no feat:* tokens is untouched), so
no real key can start throwing as a side effect of this change. The open
product question - does a package key without the bundle keep CRUD once
packages go live - stays open and is documented as such in the guide.

Implemented by a prep-ship lane (task HF-307-addon-grants); verified here:
license suite 165/165 under Jest, tsc --noEmit clean, eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdHPZAjciZFWqGa19Yf7it
…tops where it does

Three things the add-on documentation got wrong or left unsaid:

- The guide named `resumeEvaluation()` as part of the batching grant. It is the one method
  deliberately left ungated, so that losing the entitlement while evaluation is suspended
  cannot strand an engine. `batch()` is gated and was missing from the list.
- The guide claimed unconditionally that every key already grants CRUD, undo/redo, clipboard
  and batching. That is true only of keys naming no feature token — which is every key the
  generator can issue today, but the reason matters, and named expressions was missing from
  the list of areas such a key receives.
- The capability table grants the bundle four areas and not named expressions, matching what
  was scoped at the 12.08 packages meeting. Nothing said so, leaving a reader unable to tell
  a decision from a slip. Recorded on the entry itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same cleanup as on the branches below, for the comment this branch introduces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream handsontable/license-key 4.0.0 (DEV-2512) deleted src/typed-key/
and replaced the tagged key format with the entitlement key format:
<prose>, blank line, [<base64url-payload><sha512-checksum>]. The tagged
format was never issued to anyone (its 3.5.0 carrier was never released),
so the old reader is removed rather than kept alongside.

Re-vendored from src/entitlement-key/ at tag 4.0.0: detect-format and
extract-key-data are new ports; sha512 and utils are byte-identical
upstream and carry over. The reader is schema-free by upstream design,
so default-schema is no longer vendored and TIER_TO_CAPABILITY_TOKEN
(the tagged format's tier adapter) is gone with the format that fed it.

Resolution reads HyperFormula's own product entry only: capabilities
verbatim, exactly one of usage_until/release_until (the reader enforces
the shape), notice/grace, flags (trial + the three silent spellings).
Legacy 25-character keys and the literals are untouched; the invariant
stands - only a VALID entitlement key may restrict the entitlement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o
`isIsoDate` asserted `value as string` and handed it to `parseIsoDate`, which stringifies
its argument before matching the `YYYY-MM-DD` shape. The assertion is compile-time only, so
any value whose `String()` spells a date passed the check at runtime. A single-element array
is the realistic case, and it is not hypothetical: `{"usage_until": ["2099-12-31"]}` with a
valid checksum extracted cleanly, stored an `object` in a field declared `string`, and
resolved VALID.

That is the wrong direction to fail in. The payload shape is fatal by contract, so such a key
must take the invalid-key path; instead it activated a RESTRICTED entitlement, which under
our fail-open model means a malformed key grants a customer LESS than a broken one would.
`release_until` had the same hole.

Measured after the change: the array cases on both date fields return `null` from extraction
and resolve invalid, while a string date is untouched.

The mirror of this function lives upstream in `license-key`; the fix belongs there too, and
`PROVENANCE.md` should be revisited once it lands so the vendored copy does not silently
diverge from the source it pins.

Reported by Tobiadefami on this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine now resolves BOTH capability-token vocabularies in circulation,
from one source of truth: the 21 function groups of the packaging doc
(transcribed 1:1 from its §6, drift-checked by the published per-group
counts) now live in capabilities.ts, and the functions_1..4 package slices
are DERIVED from them as the doc's own cumulative group unions - so a
function moved between groups moves in both dialects at once.

New recognized tokens: fun:all (= the functions_4 grant), the 21
fun:<family>.<A|B|C> group tokens, and a fun:<CANONICAL_NAME> single-
function token for every catalog entry. fun:info.a / fun:lookup.a /
fun:offset / fun:version resolve to EMPTY grants on purpose - their
members are the protected built-ins, which must never become
table-covered. Token matching is now case-insensitive (the packaging doc
states it outright for fun:*; the other tokens tolerate it for free).

Accepting the superset is spec-clean (T7: an unrecognized token is a
grant this version does not implement, so implementing more breaks
nothing) and makes the engine robust to the still-open business choice
between the dialects. The 18.08 'fun:* grants zero functions' pin is
inverted BY DECISION (owner, 20.08), not by accident - see the test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o
… gate

The instance method now lists exactly what the instance can evaluate,
through the same listable ids and the same licenseListsFunction rule the
metadata API and the interpreter share: protected built-ins included
(OFFSET was missing before), the instance's own translation snapshot
instead of a fresh global language lookup, and no function the license
key does not include. A missing/invalid/expired key does not shorten
the list.

The static form is DEPRECATED, not removed: a static method has no key
or config in scope, so it can only ever answer for the package as a
whole - the rationale recorded on #1724, applied to the one API it
missed. Whether it is eventually removed is a separate decision and is
not taken here; the JSDoc and the changelog say deprecated, and this
commit message previously claimed removal, which was never true of the
diff. The docs build-time function count moves to an unlicensed
instance (measured: both spellings count 423 for enGB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.4.0 shipped with the static `getAvailableFunctions()` and `getFunctionDetails()`
removed, and its release notes never said so. The commit that removed them is an
ancestor of the 3.4.0 tag, the section lists only the *addition* of the instance
forms, and there is no migration guide covering 3.x - so anyone upgrading lost a
public API with nothing to point at.

Added retroactively to the 3.4.0 section rather than to Unreleased, because that is
the release the change actually shipped in. This is the same gap #1742 was opened to
close before it was closed unmerged; it belongs next to this PR's deprecation of the
sibling static method, which is the other half of the same story.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-307-registered-function-names branch from dcf9354 to 918a6cd Compare August 26, 2026 03:34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread CHANGELOG.md
- Fixed the `AVERAGEIF` function returning a division-by-zero error when the calculated average was `0`. [#1733](https://github.com/handsontable/hyperformula/pull/1733)
- Fixed the localized names of `VSTACK` and `HSTACK` in 14 language packs to match Microsoft Excel. [#1748](https://github.com/handsontable/hyperformula/pull/1748)
- Fixed the MAXPOOL and MEDIANPOOL functions throwing an uncaught `TypeError` instead of returning the `#VALUE!` error when the range dimensions are not a whole multiple of the window size and the stride. [#1718](https://github.com/handsontable/hyperformula/pull/1718)
- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog uses issue not PR

Low Severity

The new Unreleased MOD changelog bullet ends with an issue URL (issues/1747) rather than a PR link. New engine changelog entries need a pull/NNNN link; an issue-only reference is not a substitute, so this item does not match the rest of the Unreleased section.

Fix in Cursor Fix in Web

Triggered by learned rule: CHANGELOG bullets need a PR link

Reviewed by Cursor Bugbot for commit 45126e0. Configure here.

@marcin-kordas-hoc
marcin-kordas-hoc changed the base branch from hf-329-token-dialects to hf-307-entitlement-gating-pr3 September 18, 2026 10:38
@marcin-kordas-hoc marcin-kordas-hoc changed the title HF-307: align getRegisteredFunctionNames with the license gate, deprecate its static form (9/9) HF-307: align getRegisteredFunctionNames with the license gate, deprecate its static form Sep 18, 2026
Comment thread docs/guide/license-key.md
then fails.
* [`getAvailableFunctions()`](../api/classes/hyperformula.md#getavailablefunctions),
[`getFunctionDetails()`](../api/classes/hyperformula.md#getfunctiondetails) and the instance
[`getRegisteredFunctionNames()`](../api/classes/hyperformula.md#getregisteredfunctionnames)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRegisteredFunctionNames should still list ALL functions

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 67432bc. Configure here.

Comment thread CHANGELOG.md

### Removed

- **Breaking change**: Removed the static `HyperFormula.getAvailableFunctions()` and `HyperFormula.getFunctionDetails()` methods. Use the instance methods of the same names, added in this release: a static method has no engine, and therefore no configuration, in scope, so it can only answer for the package as a whole. This entry was missing from the 3.4.0 release notes and is added retroactively. [#1724](https://github.com/handsontable/hyperformula/pull/1724)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.4.0 changelog not mirrored

Medium Severity

The published 3.4.0 section now has a retroactive Removed bullet, but docs/guide/release-notes.md still has the original 3.4.0 notes. Those files are release-time mirrors, so a released-version edit in only one of them ships inconsistent notes.

Fix in Cursor Fix in Web

Triggered by learned rule: Keep CHANGELOG.md and release-notes.md in sync

Reviewed by Cursor Bugbot for commit 67432bc. Configure here.

@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.39%. Comparing base (f782d0e) to head (fab1a6f).

Additional details and impacted files

Impacted file tree graph

@@                        Coverage Diff                        @@
##           hf-307-entitlement-gating-pr3    #1743      +/-   ##
=================================================================
- Coverage                          97.39%   97.39%   -0.01%     
=================================================================
  Files                                204      204              
  Lines                              16267    16266       -1     
  Branches                            3596     3564      -32     
=================================================================
- Hits                               15844    15843       -1     
  Misses                               415      415              
  Partials                               8        8              
Files with missing lines Coverage Δ
src/HyperFormula.ts 99.76% <100.00%> (+<0.01%) ⬆️
src/interpreter/FunctionRegistry.ts 100.00% <ø> (ø)
🚀 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.

This branch has not been deployed

No deployments
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.

4 participants