Skip to content

HF-306/HF-307: resolve both capability-token dialects from one group registry (8/8) - #1741

Open
marcin-kordas-hoc wants to merge 1 commit into
hf-329-entitlement-key-envelopefrom
hf-329-token-dialects
Open

HF-306/HF-307: resolve both capability-token dialects from one group registry (8/8)#1741
marcin-kordas-hoc wants to merge 1 commit into
hf-329-entitlement-key-envelopefrom
hf-329-token-dialects

Conversation

@marcin-kordas-hoc

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

Copy link
Copy Markdown
Collaborator

8/8 of the HF-307 stack, stacked on #1740. Pairs with hyperformula-tests#42merge 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):

  • the key spec / generator vocabularyfunctions_1..4 package slices plus the spreadsheet/import_export add-ons. This is what upstream license-key@4.0.0's own schema mints today;
  • the packaging doc vocabulary (§6, 12.08, linked from HF-307 by its author as "the list of capability tokens") — fun:all, 21 group tokens fun:<family>.<A|B|C>, and a single-function token fun:<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

  • The 21 groups are transcribed 1:1 from the doc's §6 into FUNCTION_GROUPS (members verbatim, operators and protected built-ins included, so a drift check can compare against the doc's published counts).
  • The functions_1..3 package slices are now DERIVED from the groups as the doc's own cumulative unions (.A / .A+.B / .A+.B+.C), minus the operators (granted by core) and the protected built-ins. The derived memberships are pinned by name by the pre-existing capability-table.spec.ts lists, which is the consistency proof: derivation reproduces the previously authored lists exactly.
  • fun:all grants byte-for-byte what functions_4 grants; 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:VERSION resolve 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").
  • Token matching is case-insensitive (the doc states it outright for 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.
  • Alias-named tokens stay unrecognized: tokens reference canonical names, and an alias travels with its canonical function because the gates canonicalize before consulting the table (fun:STDEV.S accepts =STDEV(...)).
  • Table entry order is load-bearing for capabilityOf's first-wins reverse index: the package slices stay ahead of the new tokens, so its answers are unchanged.

⚠️ A pinned behaviour is deliberately inverted

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

  • The group registry is one more artifact to keep in sync with the packaging doc — mitigated by the drift check (per-group counts from §6) and the by-name membership pins.
  • This does not resolve the commercial 3-vs-4 package split (upstream's default schema sells Essential/Pro/Complete; the packaging doc describes four packages). Packages are generator-side data — the engine only ever sees tokens — so that's a myHOT/business call, not an engine one.
  • If the business ultimately picks a single dialect, the other one becomes dead code — cheap to keep (it's derived data, one source of truth) or to remove.

Testing

unit/license + unit/helpers/licenseKeyValidator: 16 suites / 266 tests green; dialect-equivalence pinned per package (functions_Nfun:* union, fun:allfunctions_1..4); tsc --noEmit and ESLint clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o


Note

High Risk
Changes license capability resolution, which controls which functions and API areas a key grants. A transcription or lookup bug could silently grant or deny paid features.

Overview
License keys can now grant functions using either token vocabulary: the existing package slices (functions_1functions_4 plus add-ons) or the packaging-doc fun:* groups (fun:all, fun:<family>.<A|B|C>, and per-function fun:<NAME>). Both dialects resolve from a single FUNCTION_GROUPS map so membership cannot drift.

Tokens are matched case-insensitively (trim + lowercase at lookup). Package slices are derived from the group unions instead of hand-maintained lists. Protected built-ins (OFFSET, VERSION) stay ungated: their group/function tokens are recognized but grant nothing.

Also replaces O(n²) queue.shift() expansion with a cursor walk so a key with a huge token list cannot stall construction. Feature-opt-in and unrecognized-token checks now use the same normalized spelling, so padded feat:* tokens no longer flip the all-features fallback.

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

Spec-to-ship review (2026-08-20): 3 findings fixed here

  1. An unrecognized feat:* token revoked the whole gated API instead of being inert. The
    feature 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:

    key's capabilities feature areas granted
    functions_1 5 / 5
    functions_1, feat:crud 1 / 5 — correct
    functions_1, feat:teleport (unknown) 0 / 5
    functions_1, feat:cruds (typo at issuing time) 0 / 5

    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.

  2. Group membership was pinned only by COUNT. FUNCTION_GROUPS is the single source of truth
    for 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 grants
    while leaving every count, every derived package union and every other assertion identical.
    Demonstrated live: with EXP and MEDIAN swapped between fun:math.C and fun:stat.C, the
    whole 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.

  3. Token expansion was quadratic, and whitespace split a token's meaning in two.
    CapabilityRegistry.resolve drained its queue with Array.prototype.shift — O(n) per call, so
    O(n²) overall: 1.4 s inside the constructor for a key with 100 000 tokens. Now a read cursor.
    Separately, normalizeCapabilityToken lowercased but did not trim, and the two rules that read a
    token disagreed for a padded one: ' feat:crud' failed the prefix test and was granted all five
    areas, 'feat:crud ' passed it and was granted none — neither was "crud". Normalization now
    trims, pinned by a test.

@qunabu

qunabu commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 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 4c8258e Commit Preview URL

Branch Preview URL
Aug 21 2026, 01:36 AM

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Performance comparison of head (4c8258e) vs base (523316e)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |  498.01 |  497.16 | -0.17%
                                      Sheet B |  161.38 |  158.14 | -2.01%
                                      Sheet T |   144.8 |  139.22 | -3.85%
                                Column ranges |  477.87 |  473.27 | -0.96%
                                Sorted lookup | 13984.1 | 13396.1 | -4.20%
Sheet A:  change value, add/remove row/column |   15.41 |   15.58 | +1.10%
 Sheet B: change value, add/remove row/column |  134.19 |  138.87 | +3.49%
                   Column ranges - add column |  148.62 |     154 | +3.62%
                Column ranges - without batch |  474.29 |  471.15 | -0.66%
                        Column ranges - batch |  118.68 |  117.76 | -0.78%

@marcin-kordas-hoc
marcin-kordas-hoc marked this pull request as ready for review August 20, 2026 09:10
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-329-entitlement-key-envelope branch from 864ee26 to 3ad0a5b Compare August 20, 2026 13:09
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
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.39%. Comparing base (523316e) to head (4c8258e).

Additional details and impacted files

Impacted file tree graph

@@                       Coverage Diff                        @@
##           hf-329-entitlement-key-envelope    #1741   +/-   ##
================================================================
  Coverage                            97.38%   97.39%           
================================================================
  Files                                  204      204           
  Lines                                16231    16253   +22     
  Branches                              3481     3483    +2     
================================================================
+ Hits                                 15807    15829   +22     
  Misses                                 424      424           
Files with missing lines Coverage Δ
src/license/CapabilityRegistry.ts 100.00% <100.00%> (ø)
src/license/capabilities.ts 100.00% <100.00%> (ø)
src/license/licenseResolution.ts 98.66% <100.00%> (+0.03%) ⬆️
🚀 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