Function metadata API: getAvailableFunctions / getFunctionDetails (HF-249) - #1692
Conversation
…c API (HF-249) Add static and instance getAvailableFunctions and getFunctionDetails to HyperFormula, backed by the function metadata catalogue. Instance methods delegate to the static ones using the configured language. Re-export the public catalogue types from the package entry point.
Replace the seeded catalogue with all 363 canonical built-ins, generated from docs/guide/built-in-functions.md by scripts/hf249-migrate-function-docs.ts (dev-only, not shipped). Parameter names come from the Syntax column but their count and optionality follow the implementation arity; repeating groups collapse and duplicate names are disambiguated. Also fall back to the canonical id when a language pack leaves a function name empty (e.g. SWITCH in several locales).
…-249) Add a CHANGELOG entry for the new built-in function metadata API, and a "Function metadata" section to the custom functions guide clarifying that the catalogue covers built-ins only — custom functions are absent from getAvailableFunctions() and getFunctionDetails() returns undefined for them.
Performance comparison of head (f22560e) vs base (ad142ba) |
✅ Deploy Preview for hyperformula-dev-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
8983d26 to
3f8857b
Compare
…ith getFunctionDetails (HF-249) getAvailableFunctions derived its id set from each plugin class's static implementedFunctions, so an unregistered built-in (or a registry mutation) could still appear in the list while getFunctionDetails returned undefined for it. Gate the list on FunctionRegistry.getFunctionPlugin(id) — the same resolution getFunctionDetails uses — so list and details never disagree. (Cursor Bugbot, #1692.)
…tFunctionDetails agree (HF-249) getAvailableFunctions filtered on documented+registered but not the catalogue-vs-implementation arity check that getFunctionDetails applies, so a drifted function could appear in the list yet return undefined from details. Extract resolveListableMetadata (documented + registered + arity-consistent) and use it in both. (Cursor, #1692.)
…249) Align parameter display names in the generated function-metadata catalogue with their Title-Case siblings: - financial.ts: CUMIPMT `type` -> `Type` (matches CUMPRINC); RATE `guess` -> `Guess` - statistical.ts: BESSELI/J/K/Y `x`,`n` -> `X`,`N` Also add an instance-method example to the "Function metadata" section of docs/guide/custom-functions.md so the "available both as static and instance methods" claim is backed by a snippet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Task linked: HF-249 HF function documentation available via API |
… (HF-249) Documentation accuracy fixes found while reviewing this branch: - getAvailableFunctions (static and instance): the JSDoc claimed an empty translation omits a function, so "a given language may list fewer functions than another". It does not — isFunctionTranslated tests for `undefined`, so an empty string falls back to the canonical id and all 18 bundled packs list the full 423. Describe the real behaviour. - Both instance methods resolve the language from their own config and throw LanguageNotRegisteredError if it has since been unregistered. Document it with @throws. - FunctionRegistry.isBuiltinFunctionId claimed the catalogue and the built-in id set are "equal by construction". The catalogue keys canonical ids only, so a user plugin registered directly under a built-in *alias* id resolves to itself, matches no entry, and is omitted from the static metadata API while staying callable and listed by the instance methods. Document the limitation instead of asserting the invariant. - DEV_DOCS "How to add a new function" omitted the catalogue step, so following it produced a function silently absent from the metadata API and the generated docs page. Add the step and describe both silent-omission modes (missing entry, arity drift). - The generated built-in functions page kept its "Help us improve this page" link, pointing at a gitignored artifact — following it opened GitHub's create-new-file flow, and committing through it would make the path tracked and leave every docs build dirty. Disable editLink for it. Also reverts the SHEET/SHEETS optionalArg change, which is unrelated to the metadata API and is split into its own branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…F-249)
Addresses the review of this branch. Every factual claim below was verified
against a live engine built from src/, not against Excel's documentation.
Correctness
-----------
- GAUSS stated the inverse of what it computes ("falling more than this many
times standard deviation from mean"). GAUSS(2) is 0.47725, i.e. the
probability of falling BETWEEN the mean and z; "more than" is 0.0228.
- SHEET/SHEETS declared their single argument as required while accepting a
zero-argument call, so getFunctionDetails contradicted the entry's own
"An optional sheet name..." and its first example, =SHEET(). Restores
optionalArg: true. This is metadata-only: optionalArg has exactly two
readers, and isNumberOfArgumentValuesValid is unreachable from
runFunctionWithReferenceArgument, which serves args.length === 0 itself.
The docs page prints SHEET([value]) again, as it did before this branch.
Supersedes #1717, which can be closed.
- A function id colliding with an Object.prototype member (`toString`,
`valueOf`, `__proto__`) reported as a built-in id, because the catalogue
doubles as the built-in id set and is indexed with a caller-supplied id.
A custom function registered under such a name leaked into the static list
and came back with localizedName as a function rather than a string, which
then vanished through JSON.stringify - breaking this API's own round-trip
invariant. FUNCTION_DOCS is now prototype-less, which fixes every lookup
site at once, plus a typeof guard in resolveName for the translation
package, which is indexed the same way.
Prose describes HyperFormula, not Excel
---------------------------------------
The catalogue was seeded from a page documenting Excel. INT and BASE were
corrected earlier as one-offs; these are the rest of that population:
- CEILING/FLOOR named #NUM! for significance 0; it is #DIV/0!. #NUM! occurs
only for a positive number with a negative significance, and number 0
yields 0 whatever the sign.
- CEILING.MATH/FLOOR.MATH said "when non-zero"; only mode 1 switches the
direction - mode 2 behaves like the default 0.
- nine engineering `places` descriptions were BASE's semantics: a minimum
width. They are an exact width, #NUM! when the result needs more digits,
and ignored for negative numbers.
- DECIMAL said "positive integer"; DECIMAL("0", 2) is 0.
- MOD said "one integer"; MOD(10.5, 3) is 1.5.
- MROUND omitted that base 0 returns 0.
- GCD, LCM, MULTINOMIAL, COMBIN, COMBINA and ROMAN truncate to an integer,
documented for FACT/FACTDOUBLE but not for these.
- ISEVEN/ISODD short descriptions implied a binary split; both return FALSE
for a fractional value.
- the seven *A variants claimed text counts as 0. AVERAGEA(1, "3") is 2:
numeric text coerces, non-numeric text passed directly gives #VALUE!, and
only text inside a range counts as 0.
Six of these are genuine deviations from Excel and Google Sheets and are now
rows in the list of differences, with a note on the shared root causes.
Naming and categories, before they become API surface
-----------------------------------------------------
getFunctionDetails makes ~900 parameter names public, so these are cheap now
and breaking later. Parameter counts are unchanged throughout.
- one `cumulative` flag across all 14 distributions, replacing a 6/8 split
between `boolean` (which named the type, not the role) and `mode` (which
was also carrying four unrelated meanings). TDIST's third argument is a
tail count, so it becomes `tails`.
- positional number1..number5 replaced by the statistical roles they play in
11 functions (BETA.DIST, BINOM.DIST, HYPGEOM.DIST, WEIBULL.DIST, ...).
- Excel's criteria_range1/criteria1 everywhere; COUNTIFS used `range1`,
which meant "criteria range" there and "the x values" in SUMX2MY2.
- FIND takes SEARCH's parameter names; they are the same operation.
- DAYS(end_date, start_date) and DAYS360(start_date, end_date, format) no
longer share date1/date2 for opposite roles.
- SUMX2MY2/SUMX2PY2/SUMXMY2 get array_x/array_y and three descriptions that
can actually be told apart.
- BESSELI/J/K/Y move to Engineering, N to Information, COUNTUNIQUE to
Statistical, matching where Excel and Google Sheets classify them.
- FUNCTION_CATEGORIES is alphabetical, so the generated page's sections are.
- no two of the 423 ids now share a shortDescription (was 14 groups over 32
functions, including the standard-normal pair reusing the general normal
pair's wording verbatim).
Structure
---------
- FunctionRegistry.isListableFunctionId is now the single listability gate,
applied by both getListableFunctionIds and getFunctionDetails. The design
rests on the two tiers agreeing; that was upheld by two copies of the rule
in two files.
- getAvailableFunctions' JSDoc called custom functions "instance-scoped".
registerFunctionPlugin registers globally; the static methods omit them by
policy. Reworded to say that.
- DEV_DOCS promised drift detection on "optionality that does not match
optionalArg/defaultValue". No such check exists - ParameterDoc has no
optionality field - and its absence is why the SHEET/SHEETS contradiction
shipped. Documents the real gap and the rule for zero-argument calls.
- script/*.ts is now linted (it was covered by the blanket `script` ignore,
so the branch's three new files were neither linted nor type-checked by
any npm script). They pass clean. The legacy CommonJS helpers stay ignored.
Verified: 502/502 suites, 6172 passed, 0 failed (was 2 failed of 6164);
tsc --noEmit clean; lint 0 errors; docs page still 423 rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eanups (HF-249)
Removes the shared DEFAULT_DOCUMENTATION_URL constant and its `??` fallback. FunctionDoc's
documentationUrl is now required and authored explicitly by all 370 catalogue entries, so changing a
single function's link is a one-line data edit rather than a code change, and TypeScript prevents a
new function shipping without one. Public output is unchanged: the 423 listed functions still resolve
to the same URL.
Descriptions now name parameters by their exact snake_case name wherever they refer to one:
DATEVALUE, TIMEVALUE, EDATE ("startdate"), EOMONTH ("the start date") and NOT. BETA.DIST/BETA.INV say
"beta distribution" rather than "Beta distribution", matching Excel and Google Sheets and removing the
collision with the parameter now named `beta`. A parameter describing itself in prose ("The lower
bound, rounded up to an integer") is deliberately left alone; DEV_DOCS states the rule and that
boundary so a later pass does not turn ordinary English into identifiers.
Repairs the built-in functions table of contents. The previous commit alphabetized
FUNCTION_CATEGORIES without reordering the template, so the page listed Information before Financial
while rendering the Financial section first. generate-builtin-functions-doc.ts now asserts that the
template's category list matches FUNCTION_CATEGORIES in labels and order, so that drift fails
docs:build instead of shipping.
Also corrects two stale JSDoc blocks: FUNCTION_CATEGORIES no longer claims to match the page that is
generated from it, and the repeatLastArgs example uses the real names (criteria_range, criteria).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SWITCH now names its actual parameters (value1/result1) instead of prose placeholders, states that an error candidate is skipped rather than compared, and no longer duplicates result1's default/#N/A wording. DEV_DOCS' note on the new 'Custom' category guard no longer implies it prevents every silent row loss from the generated docs page — the existing arity-drift and missing-catalogue-entry omissions are separate, pre-existing fail-safes it does not cover. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ples (HF-249) SHEET and SHEETS advertised a required argument through the metadata API while their descriptions, their examples and the engine itself all treat it as optional, so the two halves of the public API disagreed. Also corrects the repeatLastArgs guide entry, which claimed an oversized value is ignored when it actually produces an erratic set of accepted arities; documents four public-API contracts that were true but unstated (catalogue keys defining the built-in id set, case-sensitive lookup, localized names not being valid ids, and the docs page taking its function total from a different source than its rows); and makes the catalogue examples field required, matching documentationUrl. Co-authored-by: Cursor <cursoragent@cursor.com>
`generatedPagePattern` matched any path *beginning* with /guide/built-in-functions, so a future hand-written sibling page (e.g. /guide/built-in-functions-faq) would silently lose its "Help us improve this page" link even though it has an editable source file. Anchor the pattern to the one generated page it is meant for. Verified against @vuepress/shared-utils' fileToPath: guide/built-in-functions.md still resolves to /guide/built-in-functions.html and still matches, while built-in-functions-faq.html and built-in-functions.tmpl.html no longer do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…249)
The page's category list was hand-written in built-in-functions.tmpl.md
while the `### <Category>` sections were generated, and
assertTemplateCategoryListMatches() only compared the two label for label.
It therefore could not catch the case it was written to prevent: the
renderer skips a category with no catalogue entries, so a category left in
FUNCTION_CATEGORIES whose entries all vanish (a recategorization, arity
drift) shipped a contents bullet linking to an anchor no heading emits.
Emit both regions from the one pass over the categories, so a bullet exists
if and only if its section was rendered:
- renderBuiltinFunctionsMarkdown() returns {categoryList, functionSections};
- the template carries a second marker pair, AUTOGENERATED:CATEGORIES, and
spliceBuiltinFunctionsMarkdown() splices both through one marker-driven
helper, so a malformed marker state in either region still fails the build
instead of writing a half-updated page;
- assertTemplateCategoryListMatches() is deleted - the property it asserted
is now structural.
Anchors come from the slugifier VuePress actually feeds to
markdown-it-anchor, not a hand-rolled transform that would drift silently
for the first label containing punctuation or a non-ASCII letter.
A new guard rejects two rendered categories that slugify to the same anchor:
markdown-it-anchor de-duplicates colliding heading ids while the contents
list would not, so that pair would produce a dead link.
Verified: the generated page is byte-identical to before apart from the
three new marker/comment lines (13 sections, 423 rows, all 13 bullets and
anchors unchanged); the contents anchors equal the <h3 id> values VuePress
emits when the page is rendered through @vuepress/markdown; dropping a
category's entries yields neither a bullet nor a heading for it; all seven
malformed marker states throw; output is byte-idempotent across runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ranslations (HF-249) The instance-level getAvailableFunctions/getFunctionDetails resolved the translation package from the global registry, while the evaluator uses the snapshot the instance was built with (Config.translationPackage). Once the two diverge -- re-register a language code with a different package after building an instance -- the metadata API reported a localizedName the instance refuses to evaluate. buildAvailableFunctions now takes the TranslationPackage alongside the code it came from (still needed for the collator); both instance methods pass this._config.translationPackage. They no longer touch the global language registry, so the LanguageNotRegisteredError they used to throw after an unregisterLanguage is gone from their JSDoc too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…249) script/renderBuiltinFunctionsTable.ts imports @vuepress/shared-utils for the slugifier VuePress feeds to markdown-it-anchor, but the package was only a transitive dependency of vuepress - a phantom dependency that resolves through npm hoisting and would break docs:build (where the script now runs first) under any layout that stops hoisting it. Declared with the same caret range as vuepress itself so both always dedupe to the one physical copy the anchors must come from; npm ls confirms every nested requirement resolves to it, and the generation script stays green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(HF-249) The static getAvailableFunctions/getFunctionDetails described catalogue ids only, so a function registered under a brand-new id via registerFunctionPlugin was omitted - even though registration is global and the function is callable in every instance. A picker built on the static tier therefore could not see the host application's own functions, and the two tiers disagreed about which functions exist for no reason the caller could act on. Both tiers now describe every registered id. Listability is no longer decided by catalogue membership: FunctionRegistry.getListableFunctionIds delegates to getRegisteredFunctionIds, so the metadata API and getRegisteredFunctionNames answer for the same set. Which ids may wear an authored description is still a separate question, answered by the built-in ownership snapshot (isBuiltinFunction), so a custom function - including one registered over a built-in id - is reported as 'Custom' and never borrows a built-in's description. isBuiltinFunctionId and isListableFunctionId are gone with their only callers. Two behaviours follow. A user plugin registered directly under a built-in alias id (e.g. TRUNC) is now described instead of silently disappearing from the static tier. And a built-in with no catalogue entry is reported as 'Custom' rather than dropped, which makes docs:generate-function-docs fail on the missing section instead of publishing a page that quietly omits the function. Co-Authored-By: Cursor Opus 5 <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
A parameter-count disagreement between a catalogue entry and its implementation used to remove the function from getAvailableFunctions and getFunctionDetails alike, so an editing slip silently narrowed the public API. The implementation now wins instead: the details report one parameter per implemented argument under positional names and warn, while the entry still supplies the category, description, examples and link. Drift costs the parameter prose, never the function. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@cursor address all unresolved code review comments. |
|
All unresolved code review comments have been addressed: Changes made:
Verification:
The changes simplify the codebase by removing the shadowing protection mechanism as requested. |
- Revert .eslintignore change - restore -script line - Remove captureBuiltinFunctionOwners mechanism from FunctionRegistry - Remove isBuiltinFunction method and _builtinFunctionOwners map - Simplify HyperFormula.ts to use catalogue presence instead of ownership check - Update comments to reflect simplified approach Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c1b3cc5. Configure here.
The builtinPlugins array and the FunctionRegistry import were left over from the built-in ownership mechanism removed in c1b3cc5: nothing reads either any more. Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…e (HF-249) The metadata API is documented on the built-in functions page and in the API reference; repeating it next to registerFunctionPlugin only adds a copy to keep in sync. Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…ce for (HF-249) getAvailableFunctions and getFunctionDetails reported a placeholder for every field a custom (user-registered) function cannot author: an empty shortDescription, an empty documentationUrl and an empty examples array. A consumer could not tell that from a function whose author left the field blank, so the keys are now omitted and optional in the public types, as aliasOf already was. Built-in functions are unaffected: the catalogue authors all three for every entry, so their details keep the same shape. Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…(HF-249) The file name reads as if it built prose the catalogue already holds. Document what is actually assembled here: the public API objects, from the catalogue entry, the implementation's argument metadata and the active translation package. Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…t-in ids (HF-249) c1b3cc5 removed FunctionRegistry.isBuiltinFunction and with it the ownership check, so a catalogue entry is now applied by function id alone. Four JSDoc passages and DEV_DOCS still promised the old behaviour — that a plugin registered over a built-in id is reported as a custom function — and DEV_DOCS still named the deleted method. Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…249) Ordering the list under a collator derived from the requested language needed three pieces of machinery: an enGB -> en-GB conversion, a try/catch around Intl.Collator for caller-registered codes that are not valid BCP-47 tags (registerLanguage constrains nothing, so 'pt_BR' made it throw), and a warning so that fallback was not silent. Sorting with localeCompare drops all three and cannot throw on any language code. The cost is that names differing only by case or diacritics may order differently between hosts, which both public getAvailableFunctions methods now state. The docs generator drops its pinned English collator for the same comparator; the regenerated page is byte-identical here. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1692 +/- ##
===========================================
+ Coverage 97.22% 97.31% +0.09%
===========================================
Files 178 195 +17
Lines 15612 15734 +122
Branches 3430 3456 +26
===========================================
+ Hits 15178 15312 +134
+ Misses 426 414 -12
Partials 8 8
🚀 New features to boost your workflow:
|



What & why
A function picker — the Formula Builder's, or any integrator's — needs to answer two questions: what functions exist? and what do this function's arguments mean? HyperFormula could only answer the first.
getRegisteredFunctionNames()returned 423 translated names and nothing else;getAllFunctionPlugins()exposedimplementedFunctions, which has coercion rules and arity but no category, description, human-readable parameter names or examples. Everything else a picker needs existed only as prose, in the 551-line hand-maintaineddocs/guide/built-in-functions.md.And prose with no second copy drifts silently. That page published
FVSCHEDULEasFV(Pv, Schedule),RANDBETWEENasRAND(...),COLUMNasCOLUMNS(...),F.TESTwithZ.TEST's signature,T.TESTwith two of its four arguments,DAYS360's arguments reversed; it listedNORMDISTtwice (the second row was reallyNORMSDIST) and omittedVERSION, callable and counted in the page's own printed total since 2020. Every one is a copy-paste from a neighbouring row — the signature of data nothing can check against the thing it describes.So this PR moves that data into
src/and reads it back through the engine's own public API, making the published reference just its first consumer. A new authored catalogue undersrc/interpreter/functionMetadata/(370 entries, one file per category, 730 parameter descriptions and 679 examples) supplies category, description,snake_caseparameter names and descriptions, examples and a docs link;implementedFunctionsstill supplies arity and optionality. Two methods expose the join, static and per-instance:getAvailableFunctions(code)→ one cheap entry per function (localizedName,canonicalName,category,shortDescription, plusaliasOfon the 53 aliases), sorted by localized name under a collator built from that language — enough to paint all 423 picker rows without a second call.getFunctionDetails(canonicalName, code)→ adds the orderedparameters(name, description,optional),repeatLastArgs,documentationUrlandexamples. Deliberately no pre-rendered syntax string; the caller composesSUMIF(range, criteria, [sum_range])itself, andscript/formatFunctionSyntax.tsis a reference implementation of that renderer, kept out ofsrc/on purpose.docs/guide/built-in-functions.mdbecomes a build product of the public API rather than of the catalogue directly — so generating the page exercises the same alias resolution, listability gate and optionality derivation a customer's picker will. It leaves git, is gitignored, and is regenerated frombuilt-in-functions.tmpl.mdas the first step ofdocs:dev/docs:build. "The docs are wrong" and "the API is wrong" are now the same bug.The regenerated page adds
VERSION, givesNORMSDISTits own row, adds a table of contents and a per-function anchor, and corrects optionality on 27 functions the old page showed as required (IF,LOG,ROUND,SUMIF,VLOOKUP, the*2*conversions, …). Nine more change only notation: a repeating argument group is now rendered as...againstrepeatLastArgsinstead of the old hand-written[Range2, Criterion2 [, ...RangeN, CriterionN]].SWITCHwas also semantically wrong — its parameters wereexpression, value1, expression2, but the third argument is the result returned on a match.Design decisions worth a second opinion
implementedFunctionsby hand. Parameter count is cross-checked, and on a mismatch the implementation wins:getFunctionDetailsreports one parameter per implemented argument under positional names (Arg1,Arg2, …), discards the authored names and descriptions, and warns on the console naming the function. Category, description, examples and URL still come from the entry, and the function stays listed in both tiers — drift costs the parameter prose, never the availability.DEV_DOCS.mddocuments this and the remaining silent-failure mode (an entry left behind after a rename describes nothing and merely ships in the bundle).optionalcomes only fromoptionalArg/defaultValue. Hence the single production edit outside the new module:optionalArg: trueonSHEET/SHEETS, which have always accepted=SHEET()while declaring the argument required. Metadata-only and behaviour-neutral (runFunctionWithReferenceArgumentreturns before argument-count validation), and a sweep of all 423 ids found no other function with this mismatch.SUMIFis described withSUMIF's authored category and description, over its own signature. An earlier revision gated this on a snapshot of built-in plugin ownership, so a shadow reported as'Custom'; that is gone. It bought little — a plugin re-implementingSUMIFis usually still aSUMIF— and cost a module-init hook inindex.ts(the plugin identities can only come from the plugin barrel, and importing it from the registry creates a load-order cycle that breaks the bundled build) plus a second way for the whole built-in set to silently degrade to'Custom'if that hook ever failed to run. This is the bullet I'd most like a second opinion on.registerFunctionPluginis global, so a custom function is callable everywhere and the static methods list it; the instance methods list that instance's own registry, which differs when it was built with thefunctionPluginsoption. An id with no translation entry for the active language is omitted from both, because the interpreter refuses to evaluate it.shortDescription,documentationUrlandexamplesare absent (and optional in the public types, asaliasOfalready was), so a consumer can tell "no authored description" from "an empty one" and the object survivesJSON.stringifyunchanged. Built-ins are unaffected — the catalogue authors all three for every entry.FunctionListEntry,FunctionDetails,FunctionParameterDescription,FunctionCategory.FUNCTION_CATEGORIES,FunctionDocandCUSTOM_FUNCTION_CATEGORYstay internal — so a TS consumer cannot enumerate the categories to build a filter and must compare against'Custom'as a string. Worth confirming that is the right line.canonicalNameis matched exactly: case-sensitive ('sumif'→undefined, though=sumif(...)evaluates) and canonical English only. Likeliest integration pitfall for a picker holding translated names.Already reviewed
Roughly three-quarters of the develop-diff is already reviewed and merged, as sub-PRs into this branch: #1699 (page generated from the API), #1705 (HF-300: examples, docs URLs, parameter descriptions), #1709 (
snake_caseparameter names), #1710 (invalid-locale collator guard). New here: the metadata API itself, the catalogue-keyed resolution rule, custom functions in the static tier,SHEET/SHEETS, the generated table of contents, and ~55 descriptions rewritten because they documented Excel rather than HyperFormula — with the deviations added tolist-of-differences.md(INTtruncates toward zero,MODtakes the dividend's sign,ISEVEN/ISODDdon't truncate,CEILING.MATH/FLOOR.MATHhonour onlymode= 1).How I tested
Paired suite: handsontable/hyperformula-tests#14 (branch
feature/hf-249-function-metadata-api), 134 tests for this API alone, green with the full repository suite (502 files, 6,214 tests). It covers the static/instance split, i18n across all 18 packs, aliases, custom functions, plugins shadowing a built-in id or a built-in alias id, locale-aware ordering, and prototype-key ids (toString,__proto__) — plus the two invariants most worth protecting: every canonical id declared by a registered built-in plugin resolves to details, so a missing catalogue entry fails CI instead of silently dropping a function, and the list and the details always agree on which ids exist. Each guard was mutation-tested: broken deliberately, confirmed red, reverted. Assertions avoid jest-only matchers and never rely on jest ignoring a key valuedundefined, so they fail under the jasmine/karma browser job too. Separately, all 679 authored examples parse and name their own function, and a sampled slice is pinned to Excel-cross-checked values.Known trade-offs
examplesare English-spelled andOFFSETis lexed from its translated name, sogetFunctionDetails('OFFSET','deDE').examplesyields#NAME?in all 16 non-English packs — andISREF's example embedsOFFSET, returningtruein enGB butfalsein plPL with no error. The one item I'd want accepted with eyes open.SWITCHpublishesrepeatLastArgs: 1, understating its (value, result) pair group. It cannot simply become2: the field also drives runtime arity validation, and the optional trailing default needs a step of 1.documentationUrlis the same page for all 423 ids. Per-function anchors now exist on the generated page, so#${canonicalName}is a follow-up, not a redesign.HyperFormulaandFunctionRegistryboth import it eagerly.Open for the reviewer
CHANGELOG.mdnames only the two methods; it should also name the four exported types, and needs a### Changedline forSHEET/SHEETSnow reporting their argument as optional.DEV_DOCS.mdcarries general engineering policy unrelated to HF-249 (a## Performancesection, six code-style bullets, and additions to Definition of Done, Automatic tests and Documentation) — which the atomic-PR rule added in this same PR says belongs elsewhere. Split them out, or accept them explicitly.Source: https://app.clickup.com/t/9015210959/HF-249
Note
Medium Risk
Introduces a large, permanent public API and ships the full catalogue in the bundle (~25 KB gzipped), while changing how published function reference docs are produced; runtime formula evaluation is largely unchanged aside from metadata alignment (e.g. optional reporting for zero-arg reference functions).
Overview
Adds
getAvailableFunctionsandgetFunctionDetails(static and instance) so integrators can build function pickers from engine data instead of scraping docs. Metadata is authored in a new per-category catalogue undersrc/interpreter/functionMetadata/(joined withimplementedFunctionsfor arity, optionality, andrepeatLastArgs); custom functions appear with category'Custom'and positionalArgNnames unless a plugin shadows a built-in id, in which case the catalogue entry for that id still applies.The hand-maintained
docs/guide/built-in-functions.mdis removed from version control and regenerated at build time frombuilt-in-functions.tmpl.mdplus the same API (npm run docs:generate-function-docs, wired intodocs:dev/docs:build). VuePress excludes the template from routes and disables “edit this page” on the generated guide.Also exports
FunctionListEntry,FunctionDetails,FunctionParameterDescription, andFunctionCategory; documents the catalogue workflow inDEV_DOCS.md; expandsrepeatLastArgsguidance in the custom-functions guide; and records additional Excel vs HyperFormula differences inlist-of-differences.md.Reviewed by Cursor Bugbot for commit f22560e. Bugbot is set up for automated code reviews on this repo. Configure here.