HF-307 PR 4/4: describe only the functions the license grants - #1731
Open
marcin-kordas-hoc wants to merge 3 commits into
Open
HF-307 PR 4/4: describe only the functions the license grants#1731marcin-kordas-hoc wants to merge 3 commits into
marcin-kordas-hoc wants to merge 3 commits into
Conversation
Contributor
Deploying with
|
| 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 |
Performance comparison of head (e9863f2) vs base (ab1bba1) |
marcin-kordas-hoc
force-pushed
the
hf-307-entitlement-gating-pr4
branch
4 times, most recently
from
August 16, 2026 08:53
199f4f1 to
cd56f70
Compare
This was referenced Aug 18, 2026
marcin-kordas-hoc
force-pushed
the
hf-307-entitlement-gating-pr3
branch
from
August 18, 2026 08:54
05882e1 to
ab1bba1
Compare
`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
force-pushed
the
hf-307-entitlement-gating-pr4
branch
from
August 18, 2026 08:57
5d67b5d to
46ac32d
Compare
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 Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()andgetFunctionDetails()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: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 onelicenseAllowsFunctionrule 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
unrestrictedset 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.mddescribed#LIC!as only ever meaning a key problem. It now also means "not in your package".How did you test your changes?
npx tsc --noEmit: clean.npm run lint: 0 errorsnpm run docs:generate-function-docs: succeeds, and the generated reference still documentsBITAND/VLOOKUP/XIRR— the generator builds withgpl-v3, so the published docs do not narrow to a tierNot run here:
npm run test:browser(Karma needs Firefox, unavailable in this environment). No Jest-only matcher forms were used.Types of changes
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
Checklist
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 aTypeErrorout of the parser instead of returning an error value —handleOffsetHeuristic(src/parser/FormulaParser.ts:764) readsargs[0].typewith 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 acrm-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
e9863f27here; two test-level gaps fixed in the paired PR; one stale-numbers fix above. Six findings were refuted, including two independent reports thatgetFunctionPlugin()/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 implementingBITANDon acrmkey). Separately the guide said a bad key means "every function evaluates to#LIC!" and "stops formulas from calculating" — butVERSION(),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
INTwithSUMIFacross 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/VERSIONare "excluded from the listable ids entirely" — false,getListableFunctionIds()returns them, andgetAvailableFunctions()does contain both. Dropping thefunctionIsProtectedshort-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:
baseRefNamecorrect 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()andgetFunctionDetails()now list only functions the instance’s valid license actually allows—same rule as evaluation—via sharedlicenseAllowsFunctionand alias canonicalisation inlicenseListsFunction. 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.tspackage 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.