HF-307: entitlement model, capability registry, gate B (PR 1/4) - #1726
HF-307: entitlement model, capability registry, gate B (PR 1/4)#1726marcin-kordas-hoc wants to merge 1 commit into
Conversation
…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
|
Task linked: HF-307 Implement feature packages and add-ons in HF |
|
prep-flip T1 (static) — PASS Ran our internal {
"tier": "T1",
"verdict": "PASS",
"findings": [
{ "id": "T1-SNIPPETS-DRIFT-O5", "severity": "P1", "evidence": "" }
]
}
Tiers T2.5 (cross-repo contract, needs a paired |
Deploying with
|
| 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 |
|
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). |
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.ts—FeatureId,LicenseExpiry,LicenseEntitlement,unrestrictedEntitlement()src/license/capabilities.ts—CapabilityGrant,CAPABILITY_TABLE(placeholder: every built-in under a singlecoretoken, refreshed from the static function registry on everyCapabilityRegistryconstruction — see the comment on why it can't be built eagerly at module load)src/license/CapabilityRegistry.ts—resolve()(transitive, cycle-safeimpliesexpansion),capabilityOf(),allowsFunction(),allowsFeature()Modified:
src/Config.ts— license-derived state added to the existing private pool, never exposed throughgetConfig()src/interpreter/Interpreter.ts— gate B added after gate A in theFUNCTION_CALLcase, 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.ts—ErrorMessage.LicenseCapabilityThis PR ships without a real license-key payload adapter (that's a later PR), so
Configalways resolves an unrestricted entitlement today — gate B is a correct, independently-testable no-op in production until that adapter lands.FeatureId.CustomFunctionsis 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 --noEmitandtsc -p tsconfig.test.json: clean.eslinton all new/changed files: clean.test/smoke.spec.ts: passes.gpl-v3and 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.VARvs. canonicalVAR.S) gates identically to the canonical name, not the alias string;CapabilityRegistry.resolve()handles transitiveimplies, cycles, and unknown tokens correctly.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
Related issues:
Checklist:
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.CustomFunctionsas reserved or drop it entirely). Will mark ready once those are resolved.