From 660df45344255cfdda3e1452b8d2f34e37ba25a9 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 12 Aug 2026 10:07:27 +0000 Subject: [PATCH 1/3] HF-307 PR 4: describe only the functions the license grants `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 Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9 --- CHANGELOG.md | 8 +++ docs/guide/license-key.md | 29 +++++++++++ docs/guide/types-of-errors.md | 2 +- src/HyperFormula.ts | 81 ++++++++++++++++++++++++------- src/interpreter/Interpreter.ts | 5 +- src/license/CapabilityRegistry.ts | 31 ++++++++++++ 6 files changed, 134 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ecffc1c4..e879632adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added support for proprietary license keys that grant a subset of the library ("feature packages and add-ons"). A function your key does not include evaluates to a `#LIC!` error, and the corresponding parts of the API throw a `LicenseCapabilityMissingError`. Keys that grant everything, including `gpl-v3`, are unaffected. [#1728](https://github.com/handsontable/hyperformula/pull/1728) [#1729](https://github.com/handsontable/hyperformula/pull/1729) [#1730](https://github.com/handsontable/hyperformula/pull/1730) + +### Changed + +- Changed `getAvailableFunctions()` and `getFunctionDetails()` to describe only the functions the instance's license key includes, so they no longer advertise a function that would evaluate to a `#LIC!` error. A missing, invalid, or expired key does not shorten the list. [#1731](https://github.com/handsontable/hyperformula/pull/1731) + ## [3.4.0] - 2026-08-10 ### Added diff --git a/docs/guide/license-key.md b/docs/guide/license-key.md index 35ed847120..a6c441b13f 100644 --- a/docs/guide/license-key.md +++ b/docs/guide/license-key.md @@ -43,11 +43,40 @@ two dates: This process doesn't require any connection to the server. +## Feature packages and add-ons + +A proprietary license key may grant the whole library, or only part of it. If your key covers +everything you buy nothing new to think about, and neither does the GPLv3 key `gpl-v3`, which +always grants everything. + +If your key grants only part of the library, then: + +* A function your key doesn't include evaluates to a `#LIC!` error, in the same way as any other + [error value](types-of-errors.md). Everything else in the sheet keeps calculating. +* An API method your key doesn't include throws a `LicenseCapabilityMissingError` when you call + it. Methods that only read data never throw. +* [`getAvailableFunctions()`](../api/classes/hyperformula.md#getavailablefunctions) and + [`getFunctionDetails()`](../api/classes/hyperformula.md#getfunctiondetails) describe only the + functions your key includes, so a function picker built from them never offers a function that + then fails. + +Custom functions you register yourself are always available, whatever your key grants. + +::: tip +To find out which package your key includes, check your order confirmation or +[contact our team](contact.md). HyperFormula deliberately reports nothing about the contents of +your key at runtime. +::: + ## License key notifications If your license key is missing, invalid, or expired, you see a corresponding notification in the console. +In that case every function evaluates to a `#LIC!` error — but no API method starts throwing, and +`getAvailableFunctions()` still describes the full set of functions. A key problem never narrows +what the library reports it can do; it stops formulas from calculating until you fix the key. + ## License key support If you have any issues with your license key, [contact our team](contact.md). \ No newline at end of file diff --git a/docs/guide/types-of-errors.md b/docs/guide/types-of-errors.md index 7f8546a90f..1ae8c13c81 100644 --- a/docs/guide/types-of-errors.md +++ b/docs/guide/types-of-errors.md @@ -23,4 +23,4 @@ according to the language settings. | #VALUE! | Wrong type of argument | It occurs when a formula tries to improperly use different types of data. For example, you will see this error when you will try to add a string to a number. | | #CYCLE! | Circular reference | It occurs when a formula refers to its own cell, both directly and indirectly. | | #ERROR! | An error occurred | It indicates that there is an unknown error in a formula. | -| #LIC! | Invalid license key | It occurs when the license key is invalid, expired, or missing. | \ No newline at end of file +| #LIC! | License key problem | It occurs when the license key is invalid, expired, or missing, or when the function is not included in the [feature package](license-key.md#feature-packages-and-add-ons) your license key grants. | \ No newline at end of file diff --git a/src/HyperFormula.ts b/src/HyperFormula.ts index 1f57dfa293..55b6e4cfb0 100644 --- a/src/HyperFormula.ts +++ b/src/HyperFormula.ts @@ -44,7 +44,7 @@ import { import {Evaluator} from './Evaluator' import {ExportedChange, Exporter} from './Exporter' import {LicenseKeyValidityState} from './helpers/licenseKeyValidator' -import {allowsFeature} from './license/CapabilityRegistry' +import {allowsFeature, licenseAllowsFunction} from './license/CapabilityRegistry' import {FeatureId} from './license/LicenseEntitlement' import {buildTranslationPackage, RawTranslationPackage, TranslationPackage} from './i18n' import {FunctionPluginDefinition} from './interpreter' @@ -706,26 +706,57 @@ export class HyperFormula implements TypedEmitter { return {doc, metadata, aliasOf: metadataKey !== functionId ? metadataKey : undefined} } + /** + * Whether an instance's license lets it evaluate the given function id, and therefore whether the + * metadata API may describe it. Mirrors the gate-B branch the interpreter runs per function call + * (`Interpreter.evaluateAstWithoutPostprocessing`, the `FUNCTION_CALL` case), through the same + * [[licenseAllowsFunction]] rule and the same alias canonicalisation, so a listed function is + * always one that actually evaluates. + * + * Gate B only, deliberately — never the license key's validity state. A missing, invalid or expired + * key resolves to an unrestricted entitlement (the invariant `resolveLicense` documents), so it + * reaches this method with `licenseCapabilities.unrestricted` set and every function stays listed. + * That is the intended answer: a key problem is reported on the console and by `#LIC!` in cells, + * and narrowing the catalogue to the two protected built-ins would leave an integrator who has not + * wired up their key yet with an empty function picker and no clue why. The list narrows only for + * a *valid* key that genuinely does not include a function — the case where the answer is useful. + * + * @param {string} functionId - the id as registered, which may be an alias + * @param {FunctionRegistry} functionRegistry - the engine's registry, which resolves the alias map + * @param {Config} config - the instance's config, holding its resolved entitlement + */ + private static licenseListsFunction(functionId: string, functionRegistry: FunctionRegistry, config: Config): boolean { + if (!config.isLicenseGateActive || FunctionRegistry.functionIsProtected(functionId)) { + return true + } + const plugin = functionRegistry.getFunctionPlugin(functionId) + const canonicalId = plugin?.aliases?.[functionId] ?? functionId + return licenseAllowsFunction(config.capabilityRegistry, config.licenseCapabilities, canonicalId) + } + /** * Builds the function list for every id registered in an engine's own registry. Documented functions use their * catalogue entry; custom functions are listed with their name only. Sorted by localized name with * `localeCompare`, so the order follows the host's collation rules, with the language-independent canonical name * as a stable tiebreaker for entries that share a localized name. * - * Takes the [[TranslationPackage]] rather than deriving it from a language code: an instance must describe its - * functions under the package its own evaluator uses (`Config.translationPackage`), which is a snapshot taken + * Takes the instance's whole [[Config]] rather than a language code: an instance must describe its functions + * under the translation package its own evaluator uses (`Config.translationPackage`), which is a snapshot taken * when the instance was built and can differ from whatever is registered globally for the same code today. - * Deriving it here instead would let this method report a localized name the instance refuses to evaluate. + * Deriving it here instead would let this method report a localized name the instance refuses to evaluate. The + * config also carries the resolved entitlement, for the same reason — see [[licenseListsFunction]]. * * @param {FunctionRegistry} functionRegistry - the engine's registry, the source of both the ids and their plugins - * @param {TranslationPackage} language - the translation package to translate the names under + * @param {Config} config - the instance's config: the translation package and the resolved license entitlement */ - private static buildAvailableFunctions(functionRegistry: FunctionRegistry, language: TranslationPackage): FunctionListEntry[] { + private static buildAvailableFunctions(functionRegistry: FunctionRegistry, config: Config): FunctionListEntry[] { + const language = config.translationPackage const translate = (id: string) => language.getMaybeFunctionTranslation(id) return functionRegistry.getListableFunctionIds() // The interpreter refuses to evaluate ids the active language has no translation entry for // (FunctionRegistry.getFunction), so an untranslated function would be advertised but uncallable. .filter(id => language.isFunctionTranslated(id)) + .filter(id => HyperFormula.licenseListsFunction(id, functionRegistry, config)) .map(id => { const resolved = HyperFormula.resolveFunctionMetadata(id, functionRegistry.getFunctionPlugin(id)) if (resolved === undefined) { @@ -749,14 +780,19 @@ export class HyperFormula implements TypedEmitter { * * @param {string} functionId - the language-independent function id (canonical id or alias) * @param {FunctionRegistry} functionRegistry - the engine's registry, which resolves the id to its plugin - * @param {TranslationPackage} language - the translation package to translate the names under + * @param {Config} config - the instance's config: the translation package and the resolved license entitlement */ - private static buildFunctionDetailsFor(functionId: string, functionRegistry: FunctionRegistry, language: TranslationPackage): FunctionDetails | undefined { - // Mirrors the filter in buildAvailableFunctions: an id the active language cannot evaluate - // (no translation entry) gets no details either, so the list and the details always agree. + private static buildFunctionDetailsFor(functionId: string, functionRegistry: FunctionRegistry, config: Config): FunctionDetails | undefined { + const language = config.translationPackage + // Mirrors the filters in buildAvailableFunctions: an id the active language cannot evaluate + // (no translation entry), or one this instance's license does not grant, gets no details + // either, so the list and the details always agree. if (!language.isFunctionTranslated(functionId)) { return undefined } + if (!HyperFormula.licenseListsFunction(functionId, functionRegistry, config)) { + return undefined + } const resolved = HyperFormula.resolveFunctionMetadata(functionId, functionRegistry.getFunctionPlugin(functionId)) if (resolved === undefined) { return undefined @@ -4598,6 +4634,14 @@ export class HyperFormula implements TypedEmitter { * plugin registered without translations for that language. A translation set to an empty string is not a missing * entry: it falls back to the canonical id, so the function stays listed under its canonical name. * + * A function the instance's license key does not include is omitted for the same reason: it would evaluate to a + * `#LIC!` error. The list therefore answers "what can this engine compute", not "what does this package contain". + * Two consequences worth knowing: + * - A missing, invalid or expired license key does **not** shorten the list. Such a key restricts nothing by + * entitlement — it is reported on the console, and every function evaluates to `#LIC!` — so the full catalogue + * is still described. Use it to build a function picker before a key is configured. + * - Custom (user-registered) functions are never omitted; the license covers built-ins only. + * * @example * ```js * const hfInstance = HyperFormula.buildEmpty(); @@ -4611,9 +4655,9 @@ export class HyperFormula implements TypedEmitter { public getAvailableFunctions(): FunctionListEntry[] { return HyperFormula.buildAvailableFunctions( this._functionRegistry, - // The instance's own package, the one its evaluator uses — not a fresh global lookup, which could describe - // the functions under a package this instance never adopted. - this._config.translationPackage, + // The instance's own config: its translation package (not a fresh global lookup, which could describe the + // functions under a package this instance never adopted) and its resolved license entitlement. + this._config, ) } @@ -4624,9 +4668,10 @@ export class HyperFormula implements TypedEmitter { * documentation link (`documentationUrl`) and usage examples (`examples`) — every built-in authors both. * Resolves both built-in and custom (user-registered) functions, as well as aliases. An alias reports its * target's metadata (including examples, which spell the target's name) under the alias id, with the target id - * exposed as `aliasOf`. Returns `undefined` when the function id is unknown, not registered in this instance, or - * has no translation entry for the configured language (an untranslated id cannot be evaluated, so it is not - * described either, which keeps this method consistent with [[getAvailableFunctions]]). + * exposed as `aliasOf`. Returns `undefined` when the function id is unknown, not registered in this instance, has + * no translation entry for the configured language, or is not included in this instance's license key (neither an + * untranslated nor an unlicensed id can be evaluated, so neither is described — which keeps this method consistent + * with [[getAvailableFunctions]], including its behaviour for a missing, invalid or expired key). * For a custom function, `category` is `'Custom'`, there is no `shortDescription`, `documentationUrl` or * `examples`, and parameters are reported positionally (`Arg1`, `Arg2`, ...). A custom plugin registered over a * built-in id is the exception: the catalogue is keyed by function id, so it reports that built-in's authored @@ -4655,8 +4700,8 @@ export class HyperFormula implements TypedEmitter { */ public getFunctionDetails(canonicalName: string): FunctionDetails | undefined { validateArgToType(canonicalName, 'string', 'canonicalName') - // The instance's own package, the one its evaluator uses — see getAvailableFunctions. - return HyperFormula.buildFunctionDetailsFor(canonicalName, this._functionRegistry, this._config.translationPackage) + // The instance's own config, for the same reasons as getAvailableFunctions. + return HyperFormula.buildFunctionDetailsFor(canonicalName, this._functionRegistry, this._config) } /** diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts index 2c6e39f2a0..39ef01b6b1 100644 --- a/src/interpreter/Interpreter.ts +++ b/src/interpreter/Interpreter.ts @@ -13,7 +13,7 @@ import {DependencyGraph} from '../DependencyGraph' import {FormulaVertex} from '../DependencyGraph/FormulaVertex' import {ErrorMessage} from '../error-message' import {LicenseKeyValidityState} from '../helpers/licenseKeyValidator' -import {allowsFunction} from '../license/CapabilityRegistry' +import {licenseAllowsFunction} from '../license/CapabilityRegistry' import {ColumnSearchStrategy} from '../Lookup/SearchStrategy' import {Maybe} from '../Maybe' import {NamedExpressions} from '../NamedExpressions' @@ -196,8 +196,7 @@ export class Interpreter { } const canonicalId = this.canonicalFunctionId(ast.procedureName) - if (this.config.capabilityRegistry.capabilityOf(canonicalId) !== undefined - && !allowsFunction(this.config.licenseCapabilities, canonicalId)) { + if (!licenseAllowsFunction(this.config.capabilityRegistry, this.config.licenseCapabilities, canonicalId)) { return new CellError(ErrorType.LIC, ErrorMessage.LicenseCapability(ast.procedureName)) } } diff --git a/src/license/CapabilityRegistry.ts b/src/license/CapabilityRegistry.ts index 495c51cfb4..aa6eb1c60a 100644 --- a/src/license/CapabilityRegistry.ts +++ b/src/license/CapabilityRegistry.ts @@ -121,3 +121,34 @@ export function allowsFunction(resolved: ResolvedCapabilities, functionId: strin export function allowsFeature(resolved: ResolvedCapabilities, feature: FeatureId): boolean { return resolved.unrestricted || resolved.features.has(feature) } + +/** + * Whether the license lets an instance evaluate — and therefore describe — the given function. + * + * The rule both gate-B function call sites share: a function the capability table does not cover + * at all is allowed. {@link CapabilityRegistry.capabilityOf} returns `undefined` only for an id no + * token lists, which the completeness invariant in `unit/license/capability-registry.spec.ts` + * guarantees is not an unlisted built-in but a custom, instance-registered function — exempt from + * gate B by decision D1. Everything the table does cover has to be granted by the entitlement. + * + * Extracted so the interpreter and the function metadata API cannot drift apart. The metadata API + * exists to describe the functions an instance can actually evaluate, so a second spelling of this + * rule would eventually let it advertise a function that then returns `#LIC!` — the exact failure + * removing the static metadata methods (HF-349) was meant to prevent. + * + * Note this is gate B only: it says nothing about {@link LicenseKeyValidityState}. Callers that + * also need gate A check it separately, because the two gates have different answers for the same + * key — see the comment on `resolveLicense`. + * + * @param {CapabilityRegistry} registry - the registry the capabilities were resolved against + * @param {ResolvedCapabilities} resolved - the instance's resolved capabilities + * @param {string} canonicalFunctionId - the function id, already resolved through the alias map + */ +export function licenseAllowsFunction( + registry: CapabilityRegistry, + resolved: ResolvedCapabilities, + canonicalFunctionId: string, +): boolean { + return registry.capabilityOf(canonicalFunctionId) === undefined + || allowsFunction(resolved, canonicalFunctionId) +} From 46ac32d1bd276156349ad93e8c08f4d7c5b889c5 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 17 Aug 2026 06:14:11 +0000 Subject: [PATCH 2/3] HF-307 PR4: close the capability-table drift against Kuba's 12.08 doc Re-derived every function's lowest package straight from the 21 fun:. 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 Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9 --- src/license/capabilities.ts | 118 +++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/src/license/capabilities.ts b/src/license/capabilities.ts index 6c640d4440..4204d9f6bf 100644 --- a/src/license/capabilities.ts +++ b/src/license/capabilities.ts @@ -79,81 +79,87 @@ const OPERATOR_FUNCTIONS = [ /** * Package membership, as the LOWEST package that includes each function. * - * Transcribed from the packaging design's own per-function evidence file. The lists reproduce - * that file's package counts exactly (17 / 51 / 127 / 355 cumulative, plus 15 operators), which - * is how the transcript was checked; `capability-table.spec.ts` pins those counts so a later - * edit cannot drift from them silently. + * Transcribed from Kuba's 12.08 packaging doc (CU doc `8cnjcyf-33175`, page `8cnjcyf-47835`, "HF + * function groups and packages"), which supersedes the earlier evidence file this table was first + * built from. The doc organizes the catalog into 21 group tokens (`fun:.`) and + * states each package as the cumulative union of specific groups: Math engine = the `.A` groups, + * Calculated fields = `.A` + `.B`, Spreadsheet = `.A` + `.B` + `.C`. Reproducing that union gives + * 17 / 64 / 161 cumulative functions before the two protected built-ins below are removed; + * `capability-table.spec.ts` pins the resulting counts so a later edit cannot drift from them + * silently. * - * Two functions of the evidence file are deliberately absent: `OFFSET` and `VERSION` are - * protected built-ins and sit OUTSIDE the token system — the interpreter never gate-checks a - * protected function, so listing them would be dead weight that implies a restriction that does - * not exist. - * - * **This membership is a DRAFT and is expected to change before release.** The packaging design - * it follows is still under review, with the free tier's exact contents and the placement of - * several function families among the points not yet settled. + * `OFFSET` and `VERSION` are named by the doc (as `fun:lookup.A` and `fun:info.A`) but are + * deliberately absent from every list below: both are protected built-ins that sit OUTSIDE the + * token system today — the interpreter never gate-checks a protected function, so listing them + * would be dead weight that implies a restriction that does not exist. The doc calls this a + * "technical limitation" on both; see `hf-306-token-vocabulary-final` for the root cause of each + * (registry protection for VERSION, parse-time resolution for OFFSET) and what closing it would + * take. */ const MATH_ENGINE_FUNCTIONS = [ - 'ABS', 'CEILING', 'EXP', 'FLOOR', 'IF', 'LN', 'LOG', 'LOG10', 'MOD', 'POWER', 'PRODUCT', 'ROUND', - 'ROUNDDOWN', 'ROUNDUP', 'SQRT', 'SUM', + 'ABS', 'AVERAGE', 'COUNT', 'IF', 'LOG', 'MAX', 'MIN', 'MOD', 'POWER', 'PRODUCT', 'ROUND', 'ROUNDDOWN', + 'ROUNDUP', 'SQRT', 'SUM', ] /** Added by the calculated-fields package, on top of {@link MATH_ENGINE_FUNCTIONS}. */ const CALCULATED_FIELDS_FUNCTIONS = [ - 'AND', 'AVERAGE', 'CONCATENATE', 'COUNT', 'DATE', 'DATEDIF', 'DAY', 'DAYS', 'FALSE', 'FIND', 'HOUR', - 'LEFT', 'LEN', 'LOWER', 'MAX', 'MID', 'MIN', 'MINUTE', 'MONTH', 'NOT', 'NOW', 'OR', 'RIGHT', 'SEARCH', - 'SECOND', 'TEXT', 'TEXTJOIN', 'TODAY', 'TRIM', 'TRUE', 'UPPER', 'VALUE', 'XOR', 'YEAR', + 'AND', 'AVERAGEIF', 'CONCATENATE', 'COUNTIF', 'DATE', 'DATEDIF', 'DATEVALUE', 'DAY', 'DAYS', 'EOMONTH', + 'EXACT', 'FALSE', 'HOUR', 'IFS', 'ISOWEEKNUM', 'LEFT', 'LEN', 'LOWER', 'MID', 'MINUTE', 'MONTH', + 'NETWORKDAYS', 'NOT', 'OR', 'RAND', 'RANDBETWEEN', 'REPLACE', 'REPT', 'RIGHT', 'SEARCH', 'SECOND', + 'STDEV.S', 'SUBSTITUTE', 'SUMIF', 'SUMIFS', 'SWITCH', 'TEXT', 'TODAY', 'TRIM', 'TRUE', 'UPPER', 'VALUE', + 'WEEKDAY', 'WEEKNUM', 'WORKDAY', 'XOR', 'YEAR', ] /** Added by the spreadsheet package, on top of {@link CALCULATED_FIELDS_FUNCTIONS}. */ const SPREADSHEET_FUNCTIONS = [ - 'ADDRESS', 'AVERAGEIF', 'CHOOSE', 'COLUMN', 'COLUMNS', 'COUNTIF', 'COUNTIFS', 'DATEVALUE', 'DAYS360', - 'EDATE', 'EOMONTH', 'FILTER', 'FORMULATEXT', 'FV', 'HLOOKUP', 'HYPERLINK', 'IFERROR', 'IFNA', 'INDEX', - 'INTERVAL', 'IPMT', 'IRR', 'ISBINARY', 'ISBLANK', 'ISERR', 'ISERROR', 'ISEVEN', 'ISFORMULA', 'ISLOGICAL', - 'ISNA', 'ISNONTEXT', 'ISNUMBER', 'ISODD', 'ISOWEEKNUM', 'ISREF', 'ISTEXT', 'MATCH', 'MAXIFS', 'MINIFS', - 'NA', 'NETWORKDAYS', 'NETWORKDAYS.INTL', 'NPER', 'NPV', 'PERCENTILE.EXC', 'PERCENTILE.INC', 'PMT', - 'PPMT', 'PV', 'RAND', 'RANDBETWEEN', 'RATE', 'ROW', 'ROWS', 'SORT', 'STDEV.P', 'STDEV.S', 'STDEVA', - 'STDEVPA', 'SUMIF', 'SUMIFS', 'TIME', 'TIMEVALUE', 'UNIQUE', 'VAR.P', 'VAR.S', 'VARA', 'VARPA', - 'VLOOKUP', 'WEEKDAY', 'WEEKNUM', 'WORKDAY', 'WORKDAY.INTL', 'XLOOKUP', 'YEARFRAC', + 'ACOS', 'ADDRESS', 'ARRAYFORMULA', 'ARRAY_CONSTRAIN', 'ASIN', 'ATAN', 'ATAN2', 'AVERAGEA', 'CEILING', + 'CHAR', 'CHOOSE', 'CLEAN', 'CODE', 'COLUMN', 'COLUMNS', 'COS', 'COUNTA', 'COUNTBLANK', 'COUNTIFS', + 'DAYS360', 'DEC2HEX', 'EDATE', 'EVEN', 'EXP', 'FILTER', 'FIND', 'FLOOR', 'FV', 'HEX2DEC', 'HLOOKUP', + 'HSTACK', 'HYPERLINK', 'IFERROR', 'IFNA', 'INDEX', 'INT', 'IPMT', 'IRR', 'ISBLANK', 'ISERR', 'ISERROR', + 'ISEVEN', 'ISLOGICAL', 'ISNA', 'ISNUMBER', 'ISODD', 'ISTEXT', 'LARGE', 'LN', 'MATCH', 'MAXIFS', 'MEDIAN', + 'MINIFS', 'MROUND', 'N', 'NA', 'NOW', 'NPV', 'ODD', 'PERCENTILE.INC', 'PI', 'PMT', 'PPMT', 'PROPER', 'PV', + 'QUOTIENT', 'RATE', 'ROW', 'ROWS', 'SEQUENCE', 'SIGN', 'SIN', 'SLN', 'SMALL', 'SORT', 'STDEV.P', 'STDEVA', + 'STDEVPA', 'SUBTOTAL', 'SUMPRODUCT', 'SUMSQ', 'SUMXMY2', 'T', 'TAN', 'TEXTJOIN', 'TIME', 'TRANSPOSE', + 'UNICHAR', 'UNIQUE', 'VAR.P', 'VAR.S', 'VLOOKUP', 'VSTACK', 'XIRR', 'XLOOKUP', 'XNPV', 'YEARFRAC', ] /** * Added by the excel-simulator package, on top of {@link SPREADSHEET_FUNCTIONS} — the rest of the * implemented catalog. * - * Enumerated rather than taken from the function registry at run time, even though "all - * functions" would be the shorter way to say it. Reading the registry would sweep in functions - * registered through `HyperFormula.registerFunctionPlugin`, putting a user's OWN custom function - * into a paid package and returning `#LIC!` for it on a smaller licence — the opposite of HF-307 - * decision D1, which drops custom-function gating entirely. A function this table does not list - * is not gated at all, which is exactly the treatment a custom function should get. + * Kuba's doc does not itemize this remainder into groups the way it does for the first three + * packages: the excel-simulator package is stated as `fun:all` — the whole catalog, granted as a + * single token rather than assembled from named groups. This list is that remainder, enumerated + * rather than taken from the function registry at run time, even though "all functions" would be + * the shorter way to say it. Reading the registry would sweep in functions registered through + * `HyperFormula.registerFunctionPlugin`, putting a user's OWN custom function into a paid package + * and returning `#LIC!` for it on a smaller licence — the opposite of HF-307 decision D1, which + * drops custom-function gating entirely. A function this table does not list is not gated at all, + * which is exactly the treatment a custom function should get. */ const EXCEL_SIMULATOR_FUNCTIONS = [ - 'ACOS', 'ACOSH', 'ACOT', 'ACOTH', 'ARABIC', 'ARRAYFORMULA', 'ARRAY_CONSTRAIN', 'ASIN', 'ASINH', 'ATAN', - 'ATAN2', 'ATANH', 'AVEDEV', 'AVERAGEA', 'BASE', 'BESSELI', 'BESSELJ', 'BESSELK', 'BESSELY', 'BETA.DIST', - 'BETA.INV', 'BIN2DEC', 'BIN2HEX', 'BIN2OCT', 'BINOM.DIST', 'BINOM.INV', 'BITAND', 'BITLSHIFT', 'BITOR', - 'BITRSHIFT', 'BITXOR', 'CEILING.MATH', 'CEILING.PRECISE', 'CHAR', 'CHISQ.DIST', 'CHISQ.DIST.RT', - 'CHISQ.INV', 'CHISQ.INV.RT', 'CHISQ.TEST', 'CLEAN', 'CODE', 'COMBIN', 'COMBINA', 'COMPLEX', - 'CONFIDENCE.NORM', 'CONFIDENCE.T', 'CORREL', 'COS', 'COSH', 'COT', 'COTH', 'COUNTA', 'COUNTBLANK', - 'COUNTUNIQUE', 'COVARIANCE.P', 'COVARIANCE.S', 'CSC', 'CSCH', 'CUMIPMT', 'CUMPRINC', 'DAVERAGE', 'DB', - 'DCOUNT', 'DCOUNTA', 'DDB', 'DEC2BIN', 'DEC2HEX', 'DEC2OCT', 'DECIMAL', 'DEGREES', 'DELTA', 'DEVSQ', - 'DGET', 'DMAX', 'DMIN', 'DOLLARDE', 'DOLLARFR', 'DPRODUCT', 'DSTDEV', 'DSTDEVP', 'DSUM', 'DVAR', 'DVARP', - 'EFFECT', 'ERF', 'ERFC', 'EVEN', 'EXACT', 'EXPON.DIST', 'F.DIST', 'F.DIST.RT', 'F.INV', 'F.INV.RT', - 'F.TEST', 'FACT', 'FACTDOUBLE', 'FISHER', 'FISHERINV', 'FLOOR.MATH', 'FLOOR.PRECISE', 'FVSCHEDULE', - 'GAMMA', 'GAMMA.DIST', 'GAMMA.INV', 'GAMMALN', 'GAUSS', 'GCD', 'GEOMEAN', 'HARMEAN', 'HEX2BIN', - 'HEX2DEC', 'HEX2OCT', 'HSTACK', 'HYPGEOM.DIST', 'IFS', 'IMABS', 'IMAGINARY', 'IMARGUMENT', 'IMCONJUGATE', - 'IMCOS', 'IMCOSH', 'IMCOT', 'IMCSC', 'IMCSCH', 'IMDIV', 'IMEXP', 'IMLN', 'IMLOG10', 'IMLOG2', 'IMPOWER', - 'IMPRODUCT', 'IMREAL', 'IMSEC', 'IMSECH', 'IMSIN', 'IMSINH', 'IMSQRT', 'IMSUB', 'IMSUM', 'IMTAN', 'INT', - 'ISPMT', 'LARGE', 'LCM', 'LOGNORM.DIST', 'LOGNORM.INV', 'MAXA', 'MAXPOOL', 'MEDIAN', 'MEDIANPOOL', - 'MINA', 'MIRR', 'MMULT', 'MROUND', 'MULTINOMIAL', 'N', 'NEGBINOM.DIST', 'NOMINAL', 'NORM.DIST', - 'NORM.INV', 'NORM.S.DIST', 'NORM.S.INV', 'OCT2BIN', 'OCT2DEC', 'OCT2HEX', 'ODD', 'PDURATION', 'PHI', - 'PI', 'POISSON.DIST', 'PROPER', 'QUARTILE.EXC', 'QUARTILE.INC', 'QUOTIENT', 'RADIANS', 'REPLACE', 'REPT', - 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', 'SEQUENCE', 'SERIESSUM', 'SHEET', 'SHEETS', 'SIGN', 'SIN', 'SINH', - 'SKEW', 'SKEW.P', 'SLN', 'SLOPE', 'SMALL', 'SPLIT', 'SQRTPI', 'STANDARDIZE', 'STEYX', 'SUBSTITUTE', - 'SUBTOTAL', 'SUMPRODUCT', 'SUMSQ', 'SUMX2MY2', 'SUMX2PY2', 'SUMXMY2', 'SWITCH', 'SYD', 'T', 'T.DIST', - 'T.DIST.2T', 'T.DIST.RT', 'T.INV', 'T.INV.2T', 'T.TEST', 'TAN', 'TANH', 'TBILLEQ', 'TBILLPRICE', - 'TBILLYIELD', 'TDIST', 'TRANSPOSE', 'UNICHAR', 'UNICODE', 'VSTACK', 'WEIBULL.DIST', 'XIRR', 'XNPV', - 'Z.TEST', + 'ACOSH', 'ACOT', 'ACOTH', 'ARABIC', 'ASINH', 'ATANH', 'AVEDEV', 'BASE', 'BESSELI', 'BESSELJ', 'BESSELK', + 'BESSELY', 'BETA.DIST', 'BETA.INV', 'BIN2DEC', 'BIN2HEX', 'BIN2OCT', 'BINOM.DIST', 'BINOM.INV', 'BITAND', + 'BITLSHIFT', 'BITOR', 'BITRSHIFT', 'BITXOR', 'CEILING.MATH', 'CEILING.PRECISE', 'CHISQ.DIST', + 'CHISQ.DIST.RT', 'CHISQ.INV', 'CHISQ.INV.RT', 'CHISQ.TEST', 'COMBIN', 'COMBINA', 'COMPLEX', + 'CONFIDENCE.NORM', 'CONFIDENCE.T', 'CORREL', 'COSH', 'COT', 'COTH', 'COUNTUNIQUE', 'COVARIANCE.P', + 'COVARIANCE.S', 'CSC', 'CSCH', 'CUMIPMT', 'CUMPRINC', 'DAVERAGE', 'DB', 'DCOUNT', 'DCOUNTA', 'DDB', + 'DEC2BIN', 'DEC2OCT', 'DECIMAL', 'DEGREES', 'DELTA', 'DEVSQ', 'DGET', 'DMAX', 'DMIN', 'DOLLARDE', + 'DOLLARFR', 'DPRODUCT', 'DSTDEV', 'DSTDEVP', 'DSUM', 'DVAR', 'DVARP', 'EFFECT', 'ERF', 'ERFC', + 'EXPON.DIST', 'F.DIST', 'F.DIST.RT', 'F.INV', 'F.INV.RT', 'F.TEST', 'FACT', 'FACTDOUBLE', 'FISHER', + 'FISHERINV', 'FLOOR.MATH', 'FLOOR.PRECISE', 'FORMULATEXT', 'FVSCHEDULE', 'GAMMA', 'GAMMA.DIST', + 'GAMMA.INV', 'GAMMALN', 'GAUSS', 'GCD', 'GEOMEAN', 'HARMEAN', 'HEX2BIN', 'HEX2OCT', 'HYPGEOM.DIST', + 'IMABS', 'IMAGINARY', 'IMARGUMENT', 'IMCONJUGATE', 'IMCOS', 'IMCOSH', 'IMCOT', 'IMCSC', 'IMCSCH', 'IMDIV', + 'IMEXP', 'IMLN', 'IMLOG10', 'IMLOG2', 'IMPOWER', 'IMPRODUCT', 'IMREAL', 'IMSEC', 'IMSECH', 'IMSIN', + 'IMSINH', 'IMSQRT', 'IMSUB', 'IMSUM', 'IMTAN', 'INTERVAL', 'ISBINARY', 'ISFORMULA', 'ISNONTEXT', 'ISPMT', + 'ISREF', 'LCM', 'LOG10', 'LOGNORM.DIST', 'LOGNORM.INV', 'MAXA', 'MAXPOOL', 'MEDIANPOOL', 'MINA', 'MIRR', + 'MMULT', 'MULTINOMIAL', 'NEGBINOM.DIST', 'NETWORKDAYS.INTL', 'NOMINAL', 'NORM.DIST', 'NORM.INV', + 'NORM.S.DIST', 'NORM.S.INV', 'NPER', 'OCT2BIN', 'OCT2DEC', 'OCT2HEX', 'PDURATION', 'PERCENTILE.EXC', + 'PHI', 'POISSON.DIST', 'QUARTILE.EXC', 'QUARTILE.INC', 'RADIANS', 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', + 'SERIESSUM', 'SHEET', 'SHEETS', 'SINH', 'SKEW', 'SKEW.P', 'SLOPE', 'SPLIT', 'SQRTPI', 'STANDARDIZE', + 'STEYX', 'SUMX2MY2', 'SUMX2PY2', 'SYD', 'T.DIST', 'T.DIST.2T', 'T.DIST.RT', 'T.INV', 'T.INV.2T', 'T.TEST', + 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'UNICODE', 'VARA', 'VARPA', + 'WEIBULL.DIST', 'WORKDAY.INTL', 'Z.TEST', ] const coreGrant: CapabilityGrant = {functions: [...OPERATOR_FUNCTIONS], features: []} From e9863f27943a201f5caf72b136ff44d0dc3bb823 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 19 Aug 2026 08:16:02 +0000 Subject: [PATCH 3/3] HF-307 PR 4: the docs promised two things the code does not do 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 Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9 --- docs/guide/license-key.md | 15 +++++++++++---- src/HyperFormula.ts | 7 +++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/guide/license-key.md b/docs/guide/license-key.md index a6c441b13f..897c63b3d4 100644 --- a/docs/guide/license-key.md +++ b/docs/guide/license-key.md @@ -60,7 +60,10 @@ If your key grants only part of the library, then: functions your key includes, so a function picker built from them never offers a function that then fails. -Custom functions you register yourself are always available, whatever your key grants. +Custom functions you register yourself are available whatever your key grants, as long as they use +an id of their own. The licence covers built-in ids, so a plugin registered under a built-in id your +key does not include is treated as that built-in and stays unavailable — it will not be described and +it evaluates to `#LIC!`. Pick an id the built-in catalogue does not use and this cannot happen. ::: tip To find out which package your key includes, check your order confirmation or @@ -73,9 +76,13 @@ your key at runtime. If your license key is missing, invalid, or expired, you see a corresponding notification in the console. -In that case every function evaluates to a `#LIC!` error — but no API method starts throwing, and -`getAvailableFunctions()` still describes the full set of functions. A key problem never narrows -what the library reports it can do; it stops formulas from calculating until you fix the key. +In that case every function call evaluates to a `#LIC!` error — but no API method starts throwing, +and `getAvailableFunctions()` still describes the full set of functions. A key problem never +narrows what the library reports it can do. + +Arithmetic keeps working: operators such as `=A1+B1` are not function calls and are unaffected, as +are `VERSION()` and `OFFSET()`, which sit outside the licence system entirely. So a sheet with a +key problem does not go blank — it keeps producing values wherever no function is called. ## License key support diff --git a/src/HyperFormula.ts b/src/HyperFormula.ts index 55b6e4cfb0..d3b18f0010 100644 --- a/src/HyperFormula.ts +++ b/src/HyperFormula.ts @@ -4638,9 +4638,12 @@ export class HyperFormula implements TypedEmitter { * `#LIC!` error. The list therefore answers "what can this engine compute", not "what does this package contain". * Two consequences worth knowing: * - A missing, invalid or expired license key does **not** shorten the list. Such a key restricts nothing by - * entitlement — it is reported on the console, and every function evaluates to `#LIC!` — so the full catalogue + * entitlement — it is reported on the console, and every function call evaluates to `#LIC!` — so the full catalogue * is still described. Use it to build a function picker before a key is configured. - * - Custom (user-registered) functions are never omitted; the license covers built-ins only. + * - A custom (user-registered) function is omitted only if it took a built-in id the key excludes. The rule is + * "not covered by the capability table", not "not user-registered", so a plugin registered under an id the + * built-in catalogue already uses is treated as that built-in. Registered under an id of its own, a custom + * function is never omitted. See {@link getFunctionDetails}, which states the same exception. * * @example * ```js