Skip to content

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

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

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

Conversation

@marcin-kordas-hoc

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.
  • Not yet done, flagging explicitly: the private test suite (paired branch in hyperformula-tests) isn't pushed yet — I don't have that repo checked out in this environment. I also haven't run a full before/after benchmark on the interpreter hot path; the design keeps the common path to a single boolean read (isLicenseGateActive === false) replacing today's string-enum comparison, but I want real numbers in before removing draft status.

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)

Opening as a draft: the private test suite isn't pushed yet, the hot-path benchmark hasn't been run, and one design question is still open (whether to keep FeatureId.CustomFunctions as reserved or drop it entirely). Will mark ready once those are resolved.

…able#307)

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

@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

prep-flip T1 (static) — PASS

Ran our internal prep-flip review-readiness tool's T1 tier (lint / tsc / snippets-drift / fork-guard) against 0a966e35 — the mechanical checks that don't need network or paired-repo access:

{
  "tier": "T1",
  "verdict": "PASS",
  "findings": [
    { "id": "T1-SNIPPETS-DRIFT-O5", "severity": "P1", "evidence": "" }
  ]
}

lint and tsc both pass clean. The one finding is npm run snippets:check not existing as a script in this repo (unrelated tooling config, not a defect in this diff) — flagging it here rather than silently dropping it since a P1 finding shouldn't just disappear from the record.

Tiers T2.5 (cross-repo contract, needs a paired hyperformula-tests PR — none exists yet for this change), T4a (Bugbot scrape), T4b (xlsx gate), and the LLM-judgement tiers (T5a/T5b/T6) weren't run this session — no network/paired-PR access from this environment. Noting the gap rather than claiming a full run.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 11, 2026

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

@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

Superseded by #1728 — same branch, pushed directly to this repo instead of the fork so CI can access the private test-suite checkout (fork-triggered runs don't get repo secrets).

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