Skip to content

Function metadata API: getAvailableFunctions / getFunctionDetails (HF-249) - #1692

Merged
sequba merged 75 commits into
developfrom
feature/hf-249-function-metadata-api
Jul 31, 2026
Merged

Function metadata API: getAvailableFunctions / getFunctionDetails (HF-249)#1692
sequba merged 75 commits into
developfrom
feature/hf-249-function-metadata-api

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

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() exposed implementedFunctions, 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-maintained docs/guide/built-in-functions.md.

And prose with no second copy drifts silently. That page published FVSCHEDULE as FV(Pv, Schedule), RANDBETWEEN as RAND(...), COLUMN as COLUMNS(...), F.TEST with Z.TEST's signature, T.TEST with two of its four arguments, DAYS360's arguments reversed; it listed NORMDIST twice (the second row was really NORMSDIST) and omitted VERSION, 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 under src/interpreter/functionMetadata/ (370 entries, one file per category, 730 parameter descriptions and 679 examples) supplies category, description, snake_case parameter names and descriptions, examples and a docs link; implementedFunctions still supplies arity and optionality. Two methods expose the join, static and per-instance:

  • getAvailableFunctions(code) → one cheap entry per function (localizedName, canonicalName, category, shortDescription, plus aliasOf on 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 ordered parameters (name, description, optional), repeatLastArgs, documentationUrl and examples. Deliberately no pre-rendered syntax string; the caller composes SUMIF(range, criteria, [sum_range]) itself, and script/formatFunctionSyntax.ts is a reference implementation of that renderer, kept out of src/ on purpose.

docs/guide/built-in-functions.md becomes 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 from built-in-functions.tmpl.md as the first step of docs:dev/docs:build. "The docs are wrong" and "the API is wrong" are now the same bug.

The regenerated page adds VERSION, gives NORMSDIST its 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 ... against repeatLastArgs instead of the old hand-written [Range2, Criterion2 [, ...RangeN, CriterionN]]. SWITCH was also semantically wrong — its parameters were expression, value1, expression2, but the third argument is the result returned on a match.

Design decisions worth a second opinion

  • The catalogue is authored data, not derived, so it must be kept in step with implementedFunctions by hand. Parameter count is cross-checked, and on a mismatch the implementation wins: getFunctionDetails reports 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.md documents this and the remaining silent-failure mode (an entry left behind after a rename describes nothing and merely ships in the bundle).
  • Optionality is deliberately not authored or cross-checkedoptional comes only from optionalArg/defaultValue. Hence the single production edit outside the new module: optionalArg: true on SHEET/SHEETS, which have always accepted =SHEET() while declaring the argument required. Metadata-only and behaviour-neutral (runFunctionWithReferenceArgument returns before argument-count validation), and a sweep of all 423 ids found no other function with this mismatch.
  • One rule decides how a function is described: does the catalogue hold an entry for its id? The catalogue is keyed by id, not by implementation, so a user plugin registered over SUMIF is described with SUMIF'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-implementing SUMIF is usually still a SUMIF — and cost a module-init hook in index.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.
  • Both tiers describe every registered function, custom ones included. registerFunctionPlugin is 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 the functionPlugins option. An id with no translation entry for the active language is omitted from both, because the interpreter refuses to evaluate it.
  • A custom function omits the fields it cannot author, rather than reporting an empty one: shortDescription, documentationUrl and examples are absent (and optional in the public types, as aliasOf already was), so a consumer can tell "no authored description" from "an empty one" and the object survives JSON.stringify unchanged. Built-ins are unaffected — the catalogue authors all three for every entry.
  • An instance describes its functions under the translation package it was built with, not under whatever is registered globally for that code today. Otherwise re-registering a language could make the API advertise a localized name that instance refuses to evaluate.
  • Exported: FunctionListEntry, FunctionDetails, FunctionParameterDescription, FunctionCategory. FUNCTION_CATEGORIES, FunctionDoc and CUSTOM_FUNCTION_CATEGORY stay 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.
  • canonicalName is 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_case parameter 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 to list-of-differences.md (INT truncates toward zero, MOD takes the dividend's sign, ISEVEN/ISODD don't truncate, CEILING.MATH/FLOOR.MATH honour only mode = 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 valued undefined, 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

  • examples are English-spelled and OFFSET is lexed from its translated name, so getFunctionDetails('OFFSET','deDE').examples yields #NAME? in all 16 non-English packs — and ISREF's example embeds OFFSET, returning true in enGB but false in plPL with no error. The one item I'd want accepted with eyes open.
  • SWITCH publishes repeatLastArgs: 1, understating its (value, result) pair group. It cannot simply become 2: the field also drives runtime arity validation, and the optional trailing default needs a step of 1.
  • documentationUrl is 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.
  • The catalogue ships in the bundle (~25 KB gzipped) and is not tree-shakeable — HyperFormula and FunctionRegistry both import it eagerly.

Open for the reviewer

  • CHANGELOG.md names only the two methods; it should also name the four exported types, and needs a ### Changed line for SHEET/SHEETS now reporting their argument as optional.
  • DEV_DOCS.md carries general engineering policy unrelated to HF-249 (a ## Performance section, 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 getAvailableFunctions and getFunctionDetails (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 under src/interpreter/functionMetadata/ (joined with implementedFunctions for arity, optionality, and repeatLastArgs); custom functions appear with category 'Custom' and positional ArgN names 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.md is removed from version control and regenerated at build time from built-in-functions.tmpl.md plus the same API (npm run docs:generate-function-docs, wired into docs:dev / docs:build). VuePress excludes the template from routes and disables “edit this page” on the generated guide.

Also exports FunctionListEntry, FunctionDetails, FunctionParameterDescription, and FunctionCategory; documents the catalogue workflow in DEV_DOCS.md; expands repeatLastArgs guidance in the custom-functions guide; and records additional Excel vs HyperFormula differences in list-of-differences.md.

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

…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.
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Performance comparison of head (f22560e) vs base (ad142ba)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |  483.77 |   486.1 | +0.48%
                                      Sheet B |  154.56 |  152.43 | -1.38%
                                      Sheet T |     139 |  138.44 | -0.40%
                                Column ranges |  457.21 |  464.31 | +1.55%
                                Sorted lookup | 13641.9 | 13980.3 | +2.48%
Sheet A:  change value, add/remove row/column |    14.7 |   14.77 | +0.48%
 Sheet B: change value, add/remove row/column |  129.93 |  129.47 | -0.35%
                   Column ranges - add column |  146.54 |  144.39 | -1.47%
                Column ranges - without batch |  447.52 |  447.15 | -0.08%
                        Column ranges - batch |  113.41 |  110.95 | -2.17%

@netlify

netlify Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploy Preview for hyperformula-dev-docs ready!

Name Link
🔨 Latest commit f22560e
🔍 Latest deploy log https://app.netlify.com/projects/hyperformula-dev-docs/deploys/6a6c926f2142de000865d4e1
😎 Deploy Preview https://deploy-preview-1692--hyperformula-dev-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the feature/hf-249-function-metadata-api branch from 8983d26 to 3f8857b Compare June 10, 2026 03:21
marcinkordas

This comment was marked as resolved.

@marcin-kordas-hoc
marcin-kordas-hoc marked this pull request as ready for review June 10, 2026 10:54
Comment thread src/HyperFormula.ts Outdated
…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.)
Comment thread src/HyperFormula.ts Outdated
Comment thread src/HyperFormula.ts Outdated
…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.)
@marcin-kordas-hoc
marcin-kordas-hoc requested a review from sequba June 10, 2026 13:52
marcin-kordas-hoc and others added 2 commits June 23, 2026 07:45
…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>
@qunabu

qunabu commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread docs/guide/custom-functions.md Outdated
Comment thread CHANGELOG.md Outdated
… (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>
sequba and others added 9 commits July 30, 2026 13:13
…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>
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread src/interpreter/functionMetadata/FunctionDescription.ts Outdated
Comment thread .eslintignore Outdated
Comment thread src/index.ts Outdated
sequba and others added 2 commits July 31, 2026 12:13
…(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>
@sequba

sequba commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@cursor address all unresolved code review comments.

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

All unresolved code review comments have been addressed:

Changes made:

  • Reverted .eslintignore to restore the -script line
  • Removed the entire captureBuiltinFunctionOwners mechanism:
    • Removed _builtinFunctionOwners map from FunctionRegistry
    • Removed captureBuiltinFunctionOwners() method
    • Removed isBuiltinFunction() method
    • Removed the call in index.ts
  • Simplified HyperFormula.ts to check catalogue presence directly instead of ownership
  • Updated comments in FunctionRegistry.ts and functionMetadata/index.ts to reflect the simplified approach

Verification:

  • Built successfully with npm run bundle-all
  • Tested the metadata API with a verification script
  • Confirmed all built-in functions (including XIRR, VSTACK, HSTACK) return correct details
  • All 423 functions are properly listed

The changes simplify the codebase by removing the shadowing protection mechanism as requested.

Open in Web Open in Cursor 

- 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread .eslintignore Outdated
Comment thread src/index.ts Outdated
Comment thread docs/guide/custom-functions.md Outdated
Comment thread src/index.ts Outdated
Comment thread src/interpreter/functionMetadata/buildFunctionDescriptions.ts
cursoragent and others added 6 commits July 31, 2026 11:31
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>
@sequba
sequba merged commit ebaa2b2 into develop Jul 31, 2026
35 of 38 checks passed
@sequba
sequba deleted the feature/hf-249-function-metadata-api branch July 31, 2026 12:31
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.31%. Comparing base (ad142ba) to head (f22560e).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             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              
Files with missing lines Coverage Δ
src/HyperFormula.ts 99.75% <100.00%> (+0.01%) ⬆️
src/index.ts 100.00% <ø> (ø)
src/interpreter/FunctionRegistry.ts 100.00% <100.00%> (+0.62%) ⬆️
...nterpreter/functionMetadata/FunctionDescription.ts 100.00% <100.00%> (ø)
...eter/functionMetadata/buildFunctionDescriptions.ts 100.00% <100.00%> (ø)
.../functionMetadata/categories/array-manipulation.ts 100.00% <100.00%> (ø)
...nterpreter/functionMetadata/categories/database.ts 100.00% <100.00%> (ø)
...reter/functionMetadata/categories/date-and-time.ts 100.00% <100.00%> (ø)
...rpreter/functionMetadata/categories/engineering.ts 100.00% <100.00%> (ø)
...terpreter/functionMetadata/categories/financial.ts 100.00% <100.00%> (ø)
... and 11 more

... and 5 files with indirect coverage changes

🚀 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.

6 participants