Skip to content

HF-307 PR 4/4: describe only the functions the license grants - #1731

Open
marcin-kordas-hoc wants to merge 3 commits into
hf-307-entitlement-gating-pr3from
hf-307-entitlement-gating-pr4
Open

HF-307 PR 4/4: describe only the functions the license grants#1731
marcin-kordas-hoc wants to merge 3 commits into
hf-307-entitlement-gating-pr3from
hf-307-entitlement-gating-pr4

Conversation

@marcin-kordas-hoc

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

Copy link
Copy Markdown
Collaborator

Last of four. Stacks on hf-307-entitlement-gating-pr3 (#1730) — merge that first. Tests: handsontable/hyperformula-tests#33.

Context

HF-307, decision D2.

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 — precisely the failure #1724 (HF-349) removed the static variants to prevent:

the static one invites a function picker to advertise functions that then fail on evaluation, and nothing errors at integration time

That PR removed the static methods on the rationale that "an instance knows its license key, so it can answer for the engine the caller actually holds". The instance methods never learned to read it. This finishes the job.

The change

Both methods 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 of the PR as much as the filter is: two copies would drift, and the drift is invisible until a customer's function picker offers something that fails.

The one decision worth reviewing

The filter reads gate B only — never the key's validity state.

A missing, invalid or expired key resolves to an unrestricted entitlement (the invariant PR 3 documents), 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 — but it is a deliberate one, and the alternative is defensible, so it is worth an explicit look.

I chose it because narrowing on gate A would return two functions (the protected built-ins) to anyone who calls the API before configuring a key — an empty function picker with no clue why, for the exact integrator still wiring things up. A key problem is already reported on the console and by #LIC! in cells. The list narrows only for a valid key that genuinely excludes a function, which is when the answer is useful.

Pinned by tests in both directions; folding gate A into the filter fails 3 of them.

Also

  • types-of-errors.md described #LIC! as only ever meaning a key problem. It now also means "not in your package".
  • CHANGELOG — the feature's first user-visible entry. PRs 1–3 were internals by design and deliberately carry none.
  • The guide documents the mechanism, not the package contents. HF-306 is still in review with six open questions, so publishing the function lists now would put moving targets in the public docs.

How did you test your changes?

  • full suite with the private tests attached: 512 suites, 6383 tests passing, 3 skipped, 0 failures (was 6323 when this PR was opened; the stack has since been rebased onto a corrected PR 2 and PR 3 gained four test-hardening fixes)
  • npx tsc --noEmit: clean. npm run lint: 0 errors
  • npm run docs:generate-function-docs: succeeds, and the generated reference still documents BITAND / VLOOKUP / XIRR — the generator builds with gpl-v3, so the published docs do not narrow to a tier
  • mutation-tested three ways: removing the list filter fails 4 tests, dropping the custom-function exemption fails 2, folding gate A into the filter fails 3

Not run here: npm run test:browser (Karma needs Firefox, unavailable in this environment). No Jest-only matcher forms were used.

Types of changes

  • Breaking change
  • New feature or improvement
  • Bug fix
  • Additional language file
  • Change to the documentation

Not marked breaking: the narrowing only happens for a valid restricted key, and no such key can exist for a released version yet.

Related issues

  1. HF-307 — Implement feature packages and add-ons in HF (decision D2)
  2. Remove the static function metadata API (HF-349) #1724 / HF-349 — removed the static metadata API on the rationale this PR delivers
  3. Companion test PR: handsontable/hyperformula-tests#33

Checklist

  • I have reviewed the guidelines about Contributing to HyperFormula
  • 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

The three compatibility boxes are left unticked as not applicable: this change touches no formula semantics.

Found while working on this, NOT fixed here

=OFFSET() with no arguments throws a TypeError out of the parser instead of returning an error value — handleOffsetHeuristic (src/parser/FormulaParser.ts:764) reads args[0].type with no arity check. Pre-existing, unrelated to licensing, and out of scope; flagging it for a separate issue.

getRegisteredFunctionNames() still returns the whole catalogue under a restricted key — measured: 422 names on a crm-tier key, including functions that evaluate to #LIC!. Two independent reviewers in the 19.08 pass raised it and both times it was refuted on scope, not on accuracy: it is a registry/i18n surface ("what is registered, translated"), not the "what can this engine compute" surface this PR narrows, it sits outside this PR's diff, and it has a static counterpart that would have to move with it. Recording it because a function picker built on that method has exactly the problem #1724 and this PR exist to prevent, so somebody should decide deliberately rather than by omission. Not changed here.

Update 19.08 — spec-to-ship review: 5 confirmed of 11

Five dimensions, every finding adversarially verified by an agent tasked with refuting it. Two documentation defects fixed in e9863f27 here; two test-level gaps fixed in the paired PR; one stale-numbers fix above. Six findings were refuted, including two independent reports that getFunctionPlugin()/getAllFunctionPlugins() leak the catalogue — real behaviour, but no promise in this PR is broken by it.

Both docs defects were measured, not read. The guide promised custom functions are "always available, whatever your key grants" and the JSDoc added here repeated it; the real rule is "not covered by the capability table", so a plugin registered under a built-in id the key excludes is hidden and returns #LIC! (verified with an own plugin implementing BITAND on a crm key). Separately the guide said a bad key means "every function evaluates to #LIC!" and "stops formulas from calculating" — but VERSION(), OFFSET() and all arithmetic keep working, so a user whose key lapsed was told to expect a blank sheet.

The capability table was pinned by cardinality only — the size, nesting and registry-completeness checks cannot see a count-preserving move, and only 6 of ~340 entries were named anywhere in the suite. Swapping INT with SUMIF across the spreadsheet/calculated-fields boundary passed all 193 license tests and all 128 metadata-API tests; a second swap at a different boundary survived too. Each tier's sorted membership is now checked in and compared by name, so any re-derivation is a reviewable diff. This matters most for a table transcribed by hand from a doc.

The one test named for the protected built-ins asserted evaluation, not listing, justified by a comment claiming OFFSET/VERSION are "excluded from the listable ids entirely" — false, getListableFunctionIds() returns them, and getAvailableFunctions() does contain both. Dropping the functionIsProtected short-circuit and folding the two ids into the table removed them from every restricted key's picker while =OFFSET(...) still evaluated, with the suite green. Now asserted on the listing path, with a floor case for a Handsontable-only key.

Checked and clean: baseRefName correct for last-of-four, head SHAs match, all checks completed and passed on both PRs, no unresolved review threads, and no key material or internal URLs in the public diff. The one thread on the tests PR was marked resolved while the text it flagged was still wrong — that text is what finding 2 above corrects.


Note

Medium Risk
Changes public metadata behaviour for restricted proprietary keys and reshuffles which built-ins belong to each package tier; evaluation and listing stay aligned, but wrong capability data would mis-gate many functions.

Overview
getAvailableFunctions() and getFunctionDetails() now list only functions the instance’s valid license actually allows—same rule as evaluation—via shared licenseAllowsFunction and alias canonicalisation in licenseListsFunction. A restricted key no longer advertises functions that would return #LIC!.

Invalid, missing, or expired keys do not shrink the catalogue (gate B only): integrators can still build a function picker while wiring a key; cells still get #LIC! until the key is fixed.

Interpreter uses the same helper instead of duplicating the capability check. capabilities.ts package function lists are realigned to the updated packaging doc (counts 17 / 64 / 161 before protected built-ins).

Docs: #LIC! also means “not in your package”; license-key guide describes metadata behaviour; CHANGELOG updated.

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

@qunabu

qunabu commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 12, 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 e9863f2 Commit Preview URL

Branch Preview URL
Aug 19 2026, 08:19 AM

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Performance comparison of head (e9863f2) vs base (ab1bba1)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |  484.42 |  486.64 | +0.46%
                                      Sheet B |  152.82 |  153.55 | +0.48%
                                      Sheet T |  137.74 |  137.76 | +0.01%
                                Column ranges |  468.38 |  471.87 | +0.75%
                                Sorted lookup | 13951.8 | 14281.7 | +2.36%
Sheet A:  change value, add/remove row/column |   14.75 |   15.85 | +7.46%
 Sheet B: change value, add/remove row/column |  122.65 |  132.92 | +8.37%
                   Column ranges - add column |  145.59 |  146.43 | +0.58%
                Column ranges - without batch |  448.02 |  451.14 | +0.70%
                        Column ranges - batch |  110.84 |  114.78 | +3.55%

marcin-kordas-hoc and others added 2 commits August 18, 2026 08:54
`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
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-307-entitlement-gating-pr4 branch from 5d67b5d to 46ac32d Compare August 18, 2026 08:57
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
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.38%. Comparing base (ab1bba1) to head (e9863f2).

Additional details and impacted files

Impacted file tree graph

@@                      Coverage Diff                       @@
##           hf-307-entitlement-gating-pr3    #1731   +/-   ##
==============================================================
  Coverage                          97.38%   97.38%           
==============================================================
  Files                                204      204           
  Lines                              16200    16214   +14     
  Branches                            3483     3487    +4     
==============================================================
+ Hits                               15776    15790   +14     
  Misses                               424      424           
Files with missing lines Coverage Δ
src/HyperFormula.ts 99.76% <100.00%> (+<0.01%) ⬆️
src/interpreter/Interpreter.ts 95.49% <100.00%> (-0.02%) ⬇️
src/license/CapabilityRegistry.ts 100.00% <100.00%> (ø)
src/license/capabilities.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.

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.

2 participants