Skip to content

HF-307: entitlement model, capability registry, gate B (PR 1/4) - #1728

Open
marcin-kordas-hoc wants to merge 1 commit into
developfrom
hf-307-entitlement-gating-pr1
Open

HF-307: entitlement model, capability registry, gate B (PR 1/4)#1728
marcin-kordas-hoc wants to merge 1 commit into
developfrom
hf-307-entitlement-gating-pr1

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Context

HF-307 (feature packages / entitlement gating). This is PR 1 of 4: the entitlement model, the capability registry, and gate B in the interpreter. It only touches src/, has zero dependency on the (not-yet-shared) license-key package, and does not change behaviour for any key this library recognizes today (gpl-v3, legacy keys) — gate A (existing key-validity check) is untouched.

New:

  • src/license/LicenseEntitlement.tsFeatureId, LicenseExpiry, LicenseEntitlement, unrestrictedEntitlement()
  • src/license/capabilities.tsCapabilityGrant, CAPABILITY_TABLE (placeholder: every built-in under a single core token, refreshed from the static function registry on every CapabilityRegistry construction — see the comment on why it can't be built eagerly at module load)
  • src/license/CapabilityRegistry.tsresolve() (transitive, cycle-safe implies expansion), capabilityOf(), allowsFunction(), allowsFeature()

Modified:

  • src/Config.ts — license-derived state added to the existing private pool, never exposed through getConfig()
  • src/interpreter/Interpreter.ts — gate B added after gate A in the FUNCTION_CALL case, with a custom-function exemption (a function not covered by the capability table is treated as instance-registered/custom, not gated) and alias canonicalization (an alias must gate identically to its canonical function name)
  • src/error-message.tsErrorMessage.LicenseCapability

This PR ships without a real license-key payload adapter (that's a later PR), so Config always resolves an unrestricted entitlement today — gate B is a correct, independently-testable no-op in production until that adapter lands. FeatureId.CustomFunctions is kept in the enum as reserved vocabulary (no grant maps to it this release) per team decision to drop function-registration gating from this pass; happy to remove it instead if reviewers prefer.

How did you test your changes?

  • tsc --noEmit and tsc -p tsconfig.test.json: clean.
  • eslint on all new/changed files: clean.
  • test/smoke.spec.ts: passes.
  • Manual verification against the built engine (not committed, ts-node scratch scripts): gpl-v3 and missing/invalid-key behaviour unchanged (gate A untouched, exact same #LIC! messages); getConfig() does not expose the new license-derived state; a function outside a restricted entitlement's granted set returns #LIC! with the new message while a function inside it computes normally; VERSION/OFFSET (protected) bypass the gate regardless of restriction; a custom function (config.functionPlugins) is exempt from gate B; calling a gated built-in through an alias (e.g. VAR vs. canonical VAR.S) gates identically to the canonical name, not the alias string; CapabilityRegistry.resolve() handles transitive implies, cycles, and unknown tokens correctly.
    Update: private test suite pushed — hyperformula-tests#30 (unit/license/{entitlement,capability-registry,config-wiring}.spec.ts + a gate-B section appended to unit/licence.spec.ts, the 2 existing cases untouched). Against the paired branch: the new license specs pass, and the full existing suite (505 suites / 6204 tests, 0 failures) is unaffected. CI on this PR now runs the real suite (pushed directly here rather than from a fork, so the private-repo checkout has secrets access) — build/lint/unit-tests/browser-tests/Test performance/CodeQL/audit/Snyk all pass.

Task 1.5 benchmark, from this PR's own CI run (Test performance job, base 61ead730 vs head 0a966e35):

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  499.81 |  501.66 |  +0.37%
                                      Sheet B |  156.27 |  167.13 |  +6.95%
                                      Sheet T |  138.85 |  147.87 |  +6.50%
                                Column ranges |  469.57 |  485.16 |  +3.32%
                                Sorted lookup | 14182.7 | 14444.5 |  +1.85%
Sheet A:  change value, add/remove row/column |   15.32 |   17.05 | +11.29%
 Sheet B: change value, add/remove row/column |  132.54 |  139.87 |  +5.53%
                   Column ranges - add column |  147.38 |  156.86 |  +6.43%
                Column ranges - without batch |  459.94 |  464.95 |  +1.09%
                        Column ranges - batch |  114.68 |  112.74 |  -1.69%

Reporting this as-is rather than smoothing it over: Sheet A (the highest-resolution signal) is flat (+0.37%), but several rows show a consistent single-digit-percent slowdown on this one run, on shared GitHub Actions runners rather than the codespace the spike's own noise-floor numbers were calibrated against. I have not repeated the run to separate signal from noise — happy to re-run Test performance a second time before merge if reviewers want more confidence than a single sample gives. The structural claim I can stand behind: the common path (isLicenseGateActive === false, checked before gate B does anything) is one boolean read replacing today's string-enum comparison, so there's no new work on the hot path for gpl-v3/legacy keys — that part is a code-inspection fact, not a benchmark claim.

Codecov: patch coverage is 58.1% (CapabilityRegistry.ts 48.7%, Interpreter.ts 41.7%, Config.ts 71.4%) and project coverage -0.19%, both still red. This is checked against develop, but the new private tests exercise the paired branch, not this diff directly through Codecov's own accounting — worth a look before merge, though the "full suite green" result above is the stronger correctness signal.

Types of changes

  • Breaking change
  • New feature or improvement
  • Bug fix
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues:

  1. HF-307 (internal tracker; no public GitHub issue for this one)

Checklist:

  • I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project.
  • I have signed the Contributor License Agreement. (please confirm/attach on your end — I can't verify this from here)
  • My change is compliant with the OpenDocument standard. (N/A — no worksheet function behaviour changes in this PR)
  • My change is compatible with Microsoft Excel. (N/A — same reason)
  • My change is compatible with Google Sheets. (N/A — same reason)
  • I described my changes in the CHANGELOG.md file. (intentionally not done — internal-only change, no user-visible behaviour yet; a CHANGELOG entry lands with the PR that actually activates gate B for customers)
  • My changes require a documentation update. (no — nothing user-visible yet)
  • My changes require a migration guide. (no)

Ready for review. The paragraph that used to sit here said this was opening as a draft pending the private suite, the benchmark and the FeatureId.CustomFunctions question — all three are resolved: the suite is pushed (hyperformula-tests#30), the benchmark ran, and Kuba settled the token with decision D1 (dropped from this release, kept as reserved vocabulary).


Note

Medium Risk
Changes the per-function-call licensing path in the interpreter hot path, though production keys still skip gate B work via unrestricted resolution; incorrect wiring in later PRs could wrongly block formulas.

Overview
Introduces HF-307 PR 1: license entitlement types, a capability registry that expands tokens into allowed functions/features, and gate B on formula function calls—without changing behavior for keys the library already treats as fully valid today.

Config now keeps license-derived state in a private pool (licenseCapabilities, isLicenseGateActive, capabilityRegistry) and resolves unrestrictedEntitlement() until a real key payload adapter lands in a later PR. The interpreter replaces a single validity check with isLicenseGateActive, then gate A (invalid key), then gate B (per-function entitlement via allowsFunction, with alias canonicalization and an exemption when capabilityOf is undefined for custom/instance functions). A new #LIC! message covers functions not included in the license.

The placeholder core capability table refreshes built-in function ids from the static registry on each registry construction so late plugin registration stays accurate.

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

Implements tasks 1.1-1.5 of the HF-307 spec (rev 2.4): the license
entitlement model, the capability registry, and gate B in the
interpreter's FUNCTION_CALL evaluation. Gate A (existing key-validity
check) is untouched.

New:
- src/license/LicenseEntitlement.ts: FeatureId, LicenseExpiry,
  LicenseEntitlement, unrestrictedEntitlement()
- src/license/capabilities.ts: CapabilityGrant, CAPABILITY_TABLE
  (placeholder: every built-in under one 'core' token, refreshed from
  the static function registry on every CapabilityRegistry
  construction rather than at module load, since src/index.ts
  registers built-in plugins only after Config/Interpreter have
  already been evaluated)
- src/license/CapabilityRegistry.ts: resolve() (transitive,
  cycle-safe `implies` expansion), capabilityOf(), allowsFunction(),
  allowsFeature()

Modified:
- src/Config.ts: licenseCapabilities/isLicenseGateActive/
  capabilityRegistry added to the existing privatePool WeakMap, never
  exposed through getConfig()
- src/interpreter/Interpreter.ts: gate B added after gate A in the
  FUNCTION_CALL case, with a custom-function exemption predicate
  (capabilityOf() === undefined) and alias canonicalization so an
  alias gates identically to its canonical name
- src/error-message.ts: ErrorMessage.LicenseCapability

Decision deltas applied (Kuba, 2026-08-10): D1 (custom_functions
token dropped this release, FeatureId.CustomFunctions kept as
reserved vocabulary) and D3 (fail-closed + silent: an entitlement
with no recognized token grants only core, without a message,
warning, or diagnostics getter - unrestrictedEntitlement() no longer
covers that case). D2 and D4 are out of scope for this PR.

PR 1 ships without a real license-key payload adapter (PR 3), so
Config always resolves an unrestricted entitlement for now - gate B
is a correct, independently-testable no-op in production until PR 3
lands.

Full PR description, verification notes, and drafted private-repo
tests (no credentials available this session for hyperformula-tests):
marcin-kb/handoffs/hf-307-pr1-tests/

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuUdq242TtFaepKRNdEkj9
@qunabu

qunabu commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hyperformula-docs 0a966e3 Commit Preview URL

Branch Preview URL
Aug 11 2026, 09:26 AM

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Performance comparison of head (0a966e3) vs base (61ead73)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  499.81 |  501.66 |  +0.37%
                                      Sheet B |  156.27 |  167.13 |  +6.95%
                                      Sheet T |  138.85 |  147.87 |  +6.50%
                                Column ranges |  469.57 |  485.16 |  +3.32%
                                Sorted lookup | 14182.7 | 14444.5 |  +1.85%
Sheet A:  change value, add/remove row/column |   15.32 |   17.05 | +11.29%
 Sheet B: change value, add/remove row/column |  132.54 |  139.87 |  +5.53%
                   Column ranges - add column |  147.38 |  156.86 |  +6.43%
                Column ranges - without batch |  459.94 |  464.95 |  +1.09%
                        Column ranges - batch |  114.68 |  112.74 |  -1.69%

@marcin-kordas-hoc
marcin-kordas-hoc marked this pull request as ready for review August 11, 2026 10:22
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.32%. Comparing base (61ead73) to head (0a966e3).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1728      +/-   ##
===========================================
+ Coverage    97.31%   97.32%   +0.01%     
===========================================
  Files          195      198       +3     
  Lines        15719    15789      +70     
  Branches      3455     3469      +14     
===========================================
+ Hits         15297    15367      +70     
  Misses         414      414              
  Partials         8        8              
Files with missing lines Coverage Δ
src/Config.ts 94.73% <100.00%> (+0.61%) ⬆️
src/error-message.ts 100.00% <100.00%> (ø)
src/interpreter/Interpreter.ts 95.51% <100.00%> (+0.14%) ⬆️
src/license/CapabilityRegistry.ts 100.00% <100.00%> (ø)
src/license/LicenseEntitlement.ts 100.00% <100.00%> (ø)
src/license/capabilities.ts 100.00% <100.00%> (ø)
🚀 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.

2 participants