HF-306/HF-307: resolve both capability-token dialects from one group registry (8/8) - #1741
HF-306/HF-307: resolve both capability-token dialects from one group registry (8/8)#1741marcin-kordas-hoc wants to merge 1 commit into
Conversation
|
Task linked: HOT-15413 Pick function sets for each HF product package |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | 6b23a40 | Commit Preview URL Branch Preview URL |
Aug 26 2026, 03:39 AM |
Performance comparison of head (6b23a40) vs base (a425d37) |
864ee26 to
3ad0a5b
Compare
02e3434 to
e7b250b
Compare
3ad0a5b to
523316e
Compare
e7b250b to
4c8258e
Compare
Tobiadefami
left a comment
There was a problem hiding this comment.
Reviewed at 4c8258e together with the paired tests at e2a35206. I checked the 21 group memberships against the captured packaging source, package and single-function equivalence across both token dialects, case and whitespace normalization, unknown feature-token inertness, protected built-ins, reverse-index ordering, hostile-token-list performance, documentation, changelog, and current automated findings. The 16 focused license suites pass (278 tests), TypeScript passes, all current engine checks are green, and I found no material issue in this PR.
The engine now resolves BOTH capability-token vocabularies in circulation, from one source of truth: the 21 function groups of the packaging doc (transcribed 1:1 from its §6, drift-checked by the published per-group counts) now live in capabilities.ts, and the functions_1..4 package slices are DERIVED from them as the doc's own cumulative group unions - so a function moved between groups moves in both dialects at once. New recognized tokens: fun:all (= the functions_4 grant), the 21 fun:<family>.<A|B|C> group tokens, and a fun:<CANONICAL_NAME> single- function token for every catalog entry. fun:info.a / fun:lookup.a / fun:offset / fun:version resolve to EMPTY grants on purpose - their members are the protected built-ins, which must never become table-covered. Token matching is now case-insensitive (the packaging doc states it outright for fun:*; the other tokens tolerate it for free). Accepting the superset is spec-clean (T7: an unrecognized token is a grant this version does not implement, so implementing more breaks nothing) and makes the engine robust to the still-open business choice between the dialects. The 18.08 'fun:* grants zero functions' pin is inverted BY DECISION (owner, 20.08), not by accident - see the test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o
523316e to
a425d37
Compare
4c8258e to
6b23a40
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## hf-329-entitlement-key-envelope #1741 +/- ##
================================================================
Coverage 97.38% 97.39%
================================================================
Files 204 204
Lines 16233 16255 +22
Branches 3482 3484 +2
================================================================
+ Hits 15809 15831 +22
Misses 424 424
🚀 New features to boost your workflow:
|
8/8 of the HF-307 stack, stacked on #1740. Pairs with
hyperformula-tests#42— merge the tests PR first.Why
Two capability-token vocabularies exist for HyperFormula and the business choice between them is still open (D8, asked on HF-307 on 13.08, unanswered):
functions_1..4package slices plus thespreadsheet/import_exportadd-ons. This is what upstreamlicense-key@4.0.0's own schema mints today;fun:all, 21 group tokensfun:<family>.<A|B|C>, and a single-function tokenfun:<CANONICAL_NAME>for every catalog entry.Rather than bet on one, the engine now resolves both (owner's decision, 20.08). This is spec-clean: T7 defines an unrecognized token as "a grant this version does not implement", so implementing a superset breaks nothing — and whichever dialect keys end up minted in, they work without another engine release.
Design: one source of truth, two dialects reading from it
FUNCTION_GROUPS(members verbatim, operators and protected built-ins included, so a drift check can compare against the doc's published counts).functions_1..3package slices are now DERIVED from the groups as the doc's own cumulative unions (.A/.A+.B/.A+.B+.C), minus the operators (granted bycore) and the protected built-ins. The derived memberships are pinned by name by the pre-existingcapability-table.spec.tslists, which is the consistency proof: derivation reproduces the previously authored lists exactly.fun:allgrants byte-for-byte whatfunctions_4grants; the excel-simulator tail stays an authored list (the doc does not itemize it, and deriving it from the registry would gate user-registered functions — D1).fun:info.A/fun:lookup.A/fun:OFFSET/fun:VERSIONresolve to empty grants: their members are the protected built-ins, which are always available and must never become table-covered. Recognized bookkeeping, exactly as the doc frames them ("technical limitation").fun:*; the other tokens tolerate it for free — nothing collides under lowercasing). Normalization happens at lookup; entitlements keep the key's own spellings as diagnostics.fun:STDEV.Saccepts=STDEV(...)).capabilityOf's first-wins reverse index: the package slices stay ahead of the new tokens, so its answers are unchanged.The 18.08 test "a key in the published
fun:*vocabulary grants zero functions, silently" pinned a measured outcome pending D9. This PR resolves it the way the pin's own comment anticipated ("translate the vocabulary, warn, or reject — it has to change this test"): the vocabulary is now implemented. The inversion is by the owner's 20.08 decision, not a regression, and the replacement tests say so in place.Risks, stated
Testing
unit/license+unit/helpers/licenseKeyValidator: 16 suites / 266 tests green; dialect-equivalence pinned per package (functions_N≡fun:*union,fun:all≡functions_1..4);tsc --noEmitand ESLint clean.🤖 Generated with Claude Code
https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o
Note
High Risk
Changes commercial license entitlement resolution and feature gating predicates; incorrect grants or fallback logic would block or over-grant API areas and functions for paying customers.
Overview
This PR unifies two license capability vocabularies so keys minted with either
functions_1–functions_4(and add-ons) or the packaging doc’sfun:all,fun:<family>.<A|B|C>, and per-functionfun:<NAME>tokens grant the same function sets. The packaging §6 groups live inFUNCTION_GROUPS;functions_1–functions_3memberships are derived from those cumulative group unions instead of hand-maintained lists.Lookup behavior now uses
normalizeCapabilityToken(trim + lowercase) everywhere the table is consulted, includingCapabilityRegistry.resolve, which also dropsqueue.shift()for a read cursor to avoid O(n²) expansion on huge token lists.License resolution fixes two edge cases: an unrecognized
feat:*token no longer suppresses the “grant all five feature areas” fallback (only recognized feature tokens opt into explicit feature gating), and padded tokens like' feat:crud'vs'feat:crud 'no longer disagree between the feature-prefix rule and table lookup.fun:allmatchesfunctions_4; protected built-ins (OFFSET,VERSION) and their group tokens stay recognized but grant nothing; table entry order preserves priorcapabilityOfanswers for package slices.Reviewed by Cursor Bugbot for commit 6b23a40. Bugbot is set up for automated code reviews on this repo. Configure here.
Spec-to-ship review (2026-08-20): 3 findings fixed here
An unrecognized
feat:*token revoked the whole gated API instead of being inert. Thefeature opt-in trigger was a purely syntactic prefix test, so a token this version does not
implement suppressed the "this key does not talk about features" fallback and left the key with
zero of the five areas. Measured:
functions_1functions_1,feat:crudfunctions_1,feat:teleport(unknown)functions_1,feat:cruds(typo at issuing time)That inverts the additive-safety rule for the two cases that matter — an older build meeting a
key from a newer generator, and a one-character typo when a key is minted — and it contradicts
D3, which makes an unrecognized token silently ignored. The trigger now requires a feature
token this version recognizes. Four tests pin both directions.
Scope note for the reviewer: the defect predates this PR (it came in with the opt-in rule in
PR 3/9) — the adversarial verifier correctly flagged that. It is fixed here because this PR
rewrites that exact predicate to add case-insensitivity, and because leaving a known revocation
bug in place while touching the line would be the wrong call.
Group membership was pinned only by COUNT.
FUNCTION_GROUPSis the single source of truthfor both dialects, but only the doc's per-group sizes were checked — so moving a function
between two groups of the same tier letter changed what a
fun:<family>.<letter>key grantswhile leaving every count, every derived package union and every other assertion identical.
Demonstrated live: with
EXPandMEDIANswapped betweenfun:math.Candfun:stat.C, thewhole license suite stayed green at 272/272. The 21 memberships are now checked by name
against §6 of the packaging doc (the expected literal was generated by parsing the doc itself),
and that check catches the swap.
Token expansion was quadratic, and whitespace split a token's meaning in two.
CapabilityRegistry.resolvedrained its queue withArray.prototype.shift— O(n) per call, soO(n²) overall: 1.4 s inside the constructor for a key with 100 000 tokens. Now a read cursor.
Separately,
normalizeCapabilityTokenlowercased but did not trim, and the two rules that read atoken disagreed for a padded one:
' feat:crud'failed the prefix test and was granted all fiveareas,
'feat:crud 'passed it and was granted none — neither was "crud". Normalization nowtrims, pinned by a test.