Skip to content

refactor(metadata): use snake_case for all catalogue parameter names (HF-249) - #1709

Merged
sequba merged 1 commit into
feature/hf-249-function-metadata-apifrom
feature/hf-249-snake-case-param-names-2ce9
Jul 14, 2026
Merged

sequba merged 1 commit into
feature/hf-249-function-metadata-apifrom
feature/hf-249-snake-case-param-names-2ce9

Conversation

@sequba

@sequba sequba commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Context

Addresses a code-review finding on #1692: the function-metadata catalogue mixed several parameter-name conventions, which surface directly in a function-picker UI. Even closely-related functions disagreed — SUMIF used Range, Criteria, Sumrange, SUMIFS used Sum_Range, Criterion_range1, Criterion1, AVERAGEIF used Range, Criterion, Average_Range — mixing Title-case, snake_Case, and run-on tokens.

This normalizes every FunctionDoc parameter name to snake_case so the catalogue is internally consistent.

What changed

  • All 172 unique parameter names across the 13 categories/*.ts files are now snake_case (lowercase words joined by _, camelCase/acronym boundaries split, separators unified, trailing digits kept attached: Number1/Number_1number1).
  • Run-on tokens the docs left unsplit are segmented so siblings agree: Sumrangesum_range (matches SUMIFS), Searchcriterionsearch_criterion (matches VLOOKUP/HLOOKUP), plus logical_value, date_string, time_string, start_date, minimum_length, lower_bound, upper_bound, number_x, number_y.
  • The migration generator (scripts/hf249-migrate-function-docs.ts) now emits snake_case via a shared, exported toSnakeCase helper (+ a small segmentation table), so a future regeneration stays consistent. Its main() is guarded with require.main === module so the helper can be imported without side effects.
  • Not touched: parameter descriptions, examples, documentationUrl, categories, short descriptions, ordering. Custom-function positional names (Arg1, Arg2, …) are unchanged — they are a documented, self-consistent placeholder for user functions that ship no metadata (see open question below).

How did you test your changes?

  • tsc --noEmit — clean.
  • eslint on the changed files — 0 errors.
  • test/smoke.spec.ts — 4/4 pass.
  • Ad-hoc runtime checks against the built API:
    • every catalogue and getFunctionDetails parameter name matches ^[a-z][a-z0-9]*(_[a-z0-9]+)*$;
    • names are unique within each function (no collisions introduced by the transform);
    • getAvailableFunctions/getFunctionDetails list↔details parity still holds;
    • SUMIF & SUMIFS both expose sum_range; MATCH & VLOOKUP both expose search_criterion;
    • custom-function details still report Arg1, Arg2.
  • Ran the generator to confirm it produces identical parameter names to this hand-applied transform (the only regen deltas are unrelated pre-existing drift — alphabetical re-sorting of manually-added HSTACK/XIRR, the hand-authored SUM/SUMIF fields, and a stale TEXT description — so the regen output was discarded).

Types of changes

  • Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore)
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues:

  1. Follow-up to Function metadata API: getAvailableFunctions / getFunctionDetails (HF-249) #1692 (HF-249); targets that branch.

Notes for reviewers

  • Paired tests: the private test suite (hyperformula-tests#14) asserts specific parameter names (e.g. SUMIF params); those expectations need updating to the new snake_case names.
  • Open question — custom functions: the request was "snake_case for all param names". I scoped this to the catalogue and left the runtime custom-function placeholders as the documented Arg1/Arg2. If you'd also like those lowercased (arg1/arg2), it's a 1-line change in buildCustomFunctionDetails plus a custom-functions.md wording update — say the word.
  • Segmentation calls (sumrangesum_range, etc.) are the only editorial decisions here; everything else is mechanical.
Open in Web Open in Cursor 

…(HF-249)

Normalize every FunctionDoc parameter name to snake_case for a consistent
function-picker UI, replacing the mixed Title-case/underscore styles
(e.g. Sum_Range/Sumrange/Number1/Number_1). Sibling params now agree
(SUMIF & SUMIFS both use sum_range; MATCH & VLOOKUP both search_criterion).

The migration generator (scripts/hf249-migrate-function-docs.ts) now emits
snake_case via a shared toSnakeCase helper (camel/acronym splitting, separator
unification, digits kept attached) plus a small segmentation table for run-on
tokens the docs left unsplit; its main() is guarded so the helper is importable
without side effects. Descriptions, examples and documentationUrl are untouched;
custom-function positional names (Arg1, Arg2) are unchanged.

Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
@qunabu

qunabu commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Performance comparison of head (07161a0) vs base (18d4246)

                                     testName |   base |   head | change
------------------------------------------------------------------------
                                      Sheet A | 486.32 | 504.74 | +3.79%
                                      Sheet B | 157.63 | 163.53 | +3.74%
                                      Sheet T | 146.84 | 144.89 | -1.33%
                                Column ranges | 479.49 | 481.36 | +0.39%
Sheet A:  change value, add/remove row/column |  16.99 |  17.94 | +5.59%
 Sheet B: change value, add/remove row/column | 146.24 | 145.13 | -0.76%
                   Column ranges - add column | 151.86 |  164.9 | +8.59%
                Column ranges - without batch | 468.23 | 480.48 | +2.62%
                        Column ranges - batch | 119.88 | 124.31 | +3.70%

@sequba
sequba marked this pull request as ready for review July 14, 2026 13:36
@sequba
sequba merged commit 4612e75 into feature/hf-249-function-metadata-api Jul 14, 2026
28 checks passed
@sequba
sequba deleted the feature/hf-249-snake-case-param-names-2ce9 branch July 14, 2026 13:36
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.61%. Comparing base (18d4246) to head (07161a0).
⚠️ Report is 1 commits behind head on feature/hf-249-function-metadata-api.

Additional details and impacted files

Impacted file tree graph

@@                           Coverage Diff                            @@
##           feature/hf-249-function-metadata-api    #1709      +/-   ##
========================================================================
+ Coverage                                 96.42%   96.61%   +0.19%     
========================================================================
  Files                                       192      192              
  Lines                                     15585    15585              
  Branches                                   3447     3447              
========================================================================
+ Hits                                      15028    15058      +30     
+ Misses                                      557      527      -30     
Files with missing lines Coverage Δ
.../functionMetadata/categories/array-manipulation.ts 100.00% <ø> (ø)
...nterpreter/functionMetadata/categories/database.ts 100.00% <ø> (ø)
...reter/functionMetadata/categories/date-and-time.ts 100.00% <ø> (ø)
...rpreter/functionMetadata/categories/engineering.ts 100.00% <ø> (ø)
...terpreter/functionMetadata/categories/financial.ts 100.00% <ø> (ø)
...rpreter/functionMetadata/categories/information.ts 100.00% <ø> (ø)
...interpreter/functionMetadata/categories/logical.ts 100.00% <ø> (ø)
...unctionMetadata/categories/lookup-and-reference.ts 100.00% <ø> (ø)
...nctionMetadata/categories/math-and-trigonometry.ts 100.00% <ø> (ø)
...er/functionMetadata/categories/matrix-functions.ts 100.00% <ø> (ø)
... and 3 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.

marcin-kordas-hoc added a commit that referenced this pull request Jul 14, 2026
…(HF-300)

Post-merge with the updated base (snake_case params #1709 + new functions):
- Author XIRR, VSTACK, HSTACK, and fill TBILLEQ/TBILLPRICE 'discount' (base-added).
- Restore examples mis-attached during conflict resolution: SEQUENCE, XNPV, GCD.
- Remove SUM's stale per-function documentationUrl re-introduced by the merge
  (the shared .html default supplies it; comment already said it's absent).

Source: https://app.clickup.com/t/86caprtgj
ADR: adr/2026-07-13-hf300-function-metadata-enrichment.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sequba added a commit that referenced this pull request Jul 31, 2026
…-249) (#1692)

## 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-checked** —
`optional` 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](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

<!-- CURSOR_SUMMARY -->
---

> [!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`**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f22560e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
Co-authored-by: Cursor Opus 5 <noreply@cursor.com>
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.

3 participants